From 19a1f23257a234f192857994effbc08c5ab1a7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 6 Feb 2026 19:01:23 +0100 Subject: [PATCH 01/17] Add `Assert.Scope()` for soft assertions --- .../TestFramework/Assertions/Assert.Scope.cs | 29 ++++ .../TestFramework/Assertions/Assert.cs | 15 ++- .../TestFramework/Assertions/AssertScope.cs | 53 ++++++++ .../Resources/FrameworkMessages.resx | 4 + .../Resources/xlf/FrameworkMessages.cs.xlf | 5 + .../Resources/xlf/FrameworkMessages.de.xlf | 5 + .../Resources/xlf/FrameworkMessages.es.xlf | 5 + .../Resources/xlf/FrameworkMessages.fr.xlf | 5 + .../Resources/xlf/FrameworkMessages.it.xlf | 5 + .../Resources/xlf/FrameworkMessages.ja.xlf | 5 + .../Resources/xlf/FrameworkMessages.ko.xlf | 5 + .../Resources/xlf/FrameworkMessages.pl.xlf | 5 + .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 5 + .../Resources/xlf/FrameworkMessages.ru.xlf | 5 + .../Resources/xlf/FrameworkMessages.tr.xlf | 5 + .../xlf/FrameworkMessages.zh-Hans.xlf | 5 + .../xlf/FrameworkMessages.zh-Hant.xlf | 5 + .../Assertions/AssertTests.ScopeTests.cs | 127 ++++++++++++++++++ 18 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 src/TestFramework/TestFramework/Assertions/Assert.Scope.cs create mode 100644 src/TestFramework/TestFramework/Assertions/AssertScope.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs b/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs new file mode 100644 index 0000000000..a72d86767a --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// A collection of helper classes to test various conditions within +/// unit tests. If the condition being tested is not met, an exception +/// is thrown. +/// +public sealed partial class Assert +{ + /// + /// Creates a new assertion scope that collects assertion failures instead of throwing them immediately. + /// When the returned scope is disposed, all collected failures are thrown as a single . + /// + /// An representing the assertion scope. + /// + /// + /// using (Assert.Scope()) + /// { + /// Assert.AreEqual(1, 2); // collected, not thrown + /// Assert.IsTrue(false); // collected, not thrown + /// } + /// // AssertFailedException is thrown here with all collected failures. + /// + /// + public static IDisposable Scope() => new AssertScope(); +} diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 61a2e56800..1c50bd2830 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -37,8 +37,19 @@ private Assert() [DoesNotReturn] [StackTraceHidden] internal static void ThrowAssertFailed(string assertionName, string? message) - => throw new AssertFailedException( - string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); + { + var assertionFailedException = new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); + AssertScope? scope = AssertScope.Current; + if (scope is not null) + { + scope.AddError(assertionFailedException); +#pragma warning disable CS8763 // A method marked [DoesNotReturn] should not return. + return; +#pragma warning restore CS8763 // A method marked [DoesNotReturn] should not return. + } + + throw assertionFailedException; + } /// /// Builds the formatted message using the given user format message and parameters. diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs new file mode 100644 index 0000000000..8620385a58 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Represents a scope in which assertion failures are collected instead of thrown immediately. +/// When the scope is disposed, all collected failures are thrown as a single . +/// +internal sealed class AssertScope : IDisposable +{ + private static readonly AsyncLocal CurrentScope = new(); + + private readonly List _errors = []; + private readonly AssertScope? _previousScope; + private bool _disposed; + + internal AssertScope() + { + _previousScope = CurrentScope.Value; + CurrentScope.Value = this; + } + + /// + /// Gets the current active , or if no scope is active. + /// + internal static AssertScope? Current => CurrentScope.Value; + + /// + /// Adds an assertion failure message to the current scope. + /// + /// The assertion failure message. + internal void AddError(AssertFailedException error) => _errors.Add(error); + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + CurrentScope.Value = _previousScope; + + if (_errors.Count > 0) + { + throw new AssertFailedException( + string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), + new AggregateException(_errors)); + } + } +} diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 537b9331a7..49bbdba8c8 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -393,4 +393,8 @@ Actual: {2} The maximum value must be greater than or equal to the minimum value. + + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index 02a8e5a3bc..3940145d14 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -458,6 +458,11 @@ Skutečnost: {2} CollectionAssert.ReferenceEquals se nemá používat pro kontrolní výrazy. Místo toho použijte metody CollectionAssert nebo Assert.AreSame & overloads. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index b240c09df6..e83fddd970 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -458,6 +458,11 @@ Tatsächlich: {2} CollectionAssert.ReferenceEquals darf nicht für Assertions verwendet werden. Verwenden Sie stattdessen CollectionAssert-Methoden oder Assert.AreSame und Überladungen. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 427b030537..3baf586a60 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -458,6 +458,11 @@ Real: {2} CollectionAssert.ReferenceEquals no se debe usar para las aserciones. Use los métodos CollectionAssert o las sobrecargas Assert.AreSame & en su lugar. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 0e46d1985a..c7715a5b52 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -458,6 +458,11 @@ Réel : {2} CollectionAssert.ReferenceEquals ne doit pas être utilisé pour les assertions. Utilisez plutôt les méthodes CollectionAssert ou Assert.AreSame & overloads. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index bfdc4c7ee6..af5af5506d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -458,6 +458,11 @@ Effettivo: {2} Non è possibile usare CollectionAssert.ReferenceEquals per le asserzioni. In alternativa, usare i metodi CollectionAssert o Assert.AreSame e gli overload. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index bdced96434..0559241ef2 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -458,6 +458,11 @@ Actual: {2} アサーションには CollectionAssert.ReferenceEquals を使用しないでください。代わりに CollectionAssert メソッドまたは Assert.AreSame およびオーバーロードを使用してください。 + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index 6d9292a56e..e166ee47a3 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -458,6 +458,11 @@ Actual: {2} CollectionAssert.ReferenceEquals는 Assertions에 사용할 수 없습니다. 대신 CollectionAssert 메서드 또는 Assert.AreSame 및 오버로드를 사용하세요. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index c50ff5897a..5a882f4e33 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -458,6 +458,11 @@ Rzeczywiste: {2} Element Assert.ReferenceEquals nie powinien być używany dla asercji. Zamiast tego użyj metod CollectionAssert lub Assert.AreSame oraz ich przeciążeń. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index faf04cc886..29747a76f6 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -458,6 +458,11 @@ Real: {2} CollectionAssert.ReferenceEquals não deve ser usado com as Declarações. Em vez disso, use os métodos CollectionAssert ou Assert.AreSame e as sobrecargas. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index e39af70016..78d4043669 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -458,6 +458,11 @@ Actual: {2} Нельзя использовать CollectionAssert.ReferenceEquals для Assertions. Вместо этого используйте методы CollectionAssert или Assert.AreSame и перегрузки. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 3a24012694..1512ce4eff 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -458,6 +458,11 @@ Gerçekte olan: {2} CollectionAssert.ReferenceEquals, Onaylama için kullanılmamalı. Lütfen bunun yerine CollectionAssert yöntemlerini veya Assert.AreSame & aşırı yüklemelerini kullanın. + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 94100054a3..23f2af25e9 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -458,6 +458,11 @@ Actual: {2} CollectionAssert.ReferenceEquals 不应用于断言。请改用 CollectionAssert 方法或 Assert.AreSame 和重载。 + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 3d758f945a..4c455975db 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -458,6 +458,11 @@ Actual: {2} CollectionAssert.ReferenceEquals 不應使用於判斷提示。請改用 CollectionAssert 方法或 Assert.AreSame 及其多載。 + + {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope: + {0} is the number of assertion failures collected in the scope. + \ No newline at end of file diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs new file mode 100644 index 0000000000..8bfa3d994d --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public partial class AssertTests +{ + public void Scope_NoFailures_DoesNotThrow() + { + Action action = () => + { + using (Assert.Scope()) + { + Assert.IsTrue(true); + Assert.AreEqual(1, 1); + } + }; + + action.Should().NotThrow(); + } + + public void Scope_SingleFailure_ThrowsOnDispose() + { + Action action = () => + { + using (Assert.Scope()) + { + Assert.AreEqual(1, 2); + } + }; + + action.Should().Throw() + .WithMessage("*Assert.AreEqual failed*"); + } + + public void Scope_MultipleFailures_CollectsAllErrors() + { + Action action = () => + { + using (Assert.Scope()) + { + Assert.AreEqual(1, 2); + Assert.IsTrue(false); + } + }; + + action.Should().Throw() + .And.Message.Should().Contain("Assert.AreEqual failed") + .And.Contain("Assert.IsTrue failed"); + } + + public void Scope_AfterDispose_AssertionsThrowNormally() + { + // Completing a scope should restore normal behavior. + try + { + using (Assert.Scope()) + { + // intentionally empty scope + } + } + catch + { + // ignore + } + + Action action = () => Assert.IsTrue(false); + action.Should().Throw(); + } + + public void Scope_NestedScopes_InnerScopeCollectsItsOwnErrors() + { + Action action = () => + { + using (Assert.Scope()) + { + Assert.AreEqual(1, 2); // outer error + + Action innerAction = () => + { + using (Assert.Scope()) + { + Assert.IsTrue(false); // inner error + } + }; + + innerAction.Should().Throw() + .WithMessage("*Assert.IsTrue failed*"); + } + }; + + // Outer scope should only contain the outer error + action.Should().Throw() + .And.Message.Should().Contain("Assert.AreEqual failed"); + } + + public void Scope_DoubleDispose_DoesNotThrowTwice() + { + IDisposable scope = Assert.Scope(); + Assert.AreEqual(1, 2); + + Action firstDispose = () => scope.Dispose(); + firstDispose.Should().Throw(); + + // Second dispose should be a no-op + Action secondDispose = () => scope.Dispose(); + secondDispose.Should().NotThrow(); + } + + public void Scope_AssertFail_IsCollected() + { + Action action = () => + { + using (Assert.Scope()) + { + Assert.Fail("first failure"); + Assert.Fail("second failure"); + } + }; + + action.Should().Throw() + .And.Message.Should().Contain("first failure") + .And.Contain("second failure"); + } +} From 312a4a414e54f8f8be2d7127fc778dddc30acbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 6 Feb 2026 20:35:13 +0100 Subject: [PATCH 02/17] Add public API --- .../TestFramework/PublicAPI/PublicAPI.Unshipped.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index 7dc5c58110..ebaed36082 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Scope() -> System.IDisposable! From b0208b367de16f2a7f4192f05233e9d21aa23023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 9 Feb 2026 14:23:36 +0100 Subject: [PATCH 03/17] Address code review --- .../TestFramework/Assertions/Assert.Scope.cs | 3 +++ .../TestFramework/Assertions/AssertScope.cs | 24 +++++++++++++++---- .../PublicAPI/PublicAPI.Unshipped.txt | 2 +- .../Resources/FrameworkMessages.resx | 3 +++ .../Resources/xlf/FrameworkMessages.cs.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.de.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.es.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.fr.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.it.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.ja.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.ko.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.pl.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.ru.xlf | 5 ++++ .../Resources/xlf/FrameworkMessages.tr.xlf | 5 ++++ .../xlf/FrameworkMessages.zh-Hans.xlf | 5 ++++ .../xlf/FrameworkMessages.zh-Hant.xlf | 5 ++++ 17 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs b/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs index a72d86767a..2c0c3411cd 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// @@ -25,5 +27,6 @@ public sealed partial class Assert /// // AssertFailedException is thrown here with all collected failures. /// /// + [Experimental("MSTESTEXP", UrlFormat = "https://aka.ms/mstest/diagnostics#{0}")] public static IDisposable Scope() => new AssertScope(); } diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs index 8620385a58..d8e8cdeb86 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertScope.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Collections.Concurrent; + namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// @@ -11,13 +13,16 @@ internal sealed class AssertScope : IDisposable { private static readonly AsyncLocal CurrentScope = new(); - private readonly List _errors = []; - private readonly AssertScope? _previousScope; + private readonly ConcurrentQueue _errors = new(); private bool _disposed; internal AssertScope() { - _previousScope = CurrentScope.Value; + if (CurrentScope.Value is not null) + { + throw new InvalidOperationException(FrameworkMessages.AssertScopeNestedNotAllowed); + } + CurrentScope.Value = this; } @@ -30,7 +35,11 @@ internal AssertScope() /// Adds an assertion failure message to the current scope. /// /// The assertion failure message. - internal void AddError(AssertFailedException error) => _errors.Add(error); + internal void AddError(AssertFailedException error) + { + ObjectDisposedException.ThrowIf(_disposed, this); + _errors.Enqueue(error); + } /// public void Dispose() @@ -41,7 +50,12 @@ public void Dispose() } _disposed = true; - CurrentScope.Value = _previousScope; + CurrentScope.Value = null; + + if (_errors.Count == 1 && _errors.TryDequeue(out AssertFailedException? singleError)) + { + throw singleError; + } if (_errors.Count > 0) { diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index ebaed36082..ee81f202d3 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ #nullable enable -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Scope() -> System.IDisposable! +[MSTESTEXP]static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Scope() -> System.IDisposable! diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 49bbdba8c8..bf8802e3ee 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -397,4 +397,7 @@ Actual: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index 3940145d14..31747db419 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -463,6 +463,11 @@ Skutečnost: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index e83fddd970..ac053b5a13 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -463,6 +463,11 @@ Tatsächlich: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 3baf586a60..48f70b8219 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -463,6 +463,11 @@ Real: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index c7715a5b52..246b8a2447 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -463,6 +463,11 @@ Réel : {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index af5af5506d..ef6a99d45c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -463,6 +463,11 @@ Effettivo: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index 0559241ef2..d8bd32459b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -463,6 +463,11 @@ Actual: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index e166ee47a3..4bf2938b50 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -463,6 +463,11 @@ Actual: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 5a882f4e33..64e08aac69 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -463,6 +463,11 @@ Rzeczywiste: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index 29747a76f6..3bf27131b9 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -463,6 +463,11 @@ Real: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 78d4043669..118a2af866 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -463,6 +463,11 @@ Actual: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 1512ce4eff..61bb9cc180 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -463,6 +463,11 @@ Gerçekte olan: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 23f2af25e9..32d51b44ac 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -463,6 +463,11 @@ Actual: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 4c455975db..652d0fb46f 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -463,6 +463,11 @@ Actual: {2} {0} assertion(s) failed within the assert scope: {0} is the number of assertion failures collected in the scope. + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + \ No newline at end of file From 6133a2578f737092c8959ebbdfc20d51f1682227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 9 Feb 2026 17:15:22 +0100 Subject: [PATCH 04/17] Fixes --- .../TestFramework/Assertions/Assert.Scope.cs | 2 -- .../TestFramework/Assertions/AssertScope.cs | 12 ++++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs b/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs index 2c0c3411cd..d85b3c2fa8 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Diagnostics.CodeAnalysis; - namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs index d8e8cdeb86..986dd3c84a 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertScope.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Collections.Concurrent; - namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// @@ -37,7 +35,13 @@ internal AssertScope() /// The assertion failure message. internal void AddError(AssertFailedException error) { - ObjectDisposedException.ThrowIf(_disposed, this); +#pragma warning disable CA1513 // Use ObjectDisposedException throw helper + if (_disposed) + { + throw new ObjectDisposedException(nameof(AssertScope)); + } +#pragma warning restore CA1513 // Use ObjectDisposedException throw helper + _errors.Enqueue(error); } @@ -57,7 +61,7 @@ public void Dispose() throw singleError; } - if (_errors.Count > 0) + if (!_errors.IsEmpty) { throw new AssertFailedException( string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), From 12d69aff2ea8bc14491869130624e812b0af6fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 9 Feb 2026 22:31:55 +0100 Subject: [PATCH 05/17] Update --- .../011-Soft-Assertions-Nullability-Design.md | 263 ++++++++++++++++++ .../Assertions/Assert.AreEqual.cs | 15 +- .../Assertions/Assert.AreSame.cs | 6 +- .../Assertions/Assert.Contains.cs | 22 +- .../TestFramework/Assertions/Assert.Count.cs | 6 +- .../Assertions/Assert.EndsWith.cs | 4 +- .../TestFramework/Assertions/Assert.Fail.cs | 2 +- .../Assertions/Assert.IComparable.cs | 18 +- .../Assert.IsExactInstanceOfType.cs | 11 +- .../Assertions/Assert.IsInstanceOfType.cs | 11 +- .../TestFramework/Assertions/Assert.IsNull.cs | 4 +- .../TestFramework/Assertions/Assert.IsTrue.cs | 13 +- .../Assertions/Assert.Matches.cs | 4 +- .../Assertions/Assert.StartsWith.cs | 4 +- .../Assertions/Assert.ThrowsException.cs | 8 +- .../TestFramework/Assertions/Assert.cs | 68 +++-- .../TestFramework/Assertions/AssertScope.cs | 9 +- .../Assertions/CollectionAssert.cs | 38 +-- .../TestFramework/Assertions/StringAssert.cs | 10 +- .../Resources/FrameworkMessages.resx | 2 +- .../Resources/xlf/FrameworkMessages.cs.xlf | 4 +- .../Resources/xlf/FrameworkMessages.de.xlf | 4 +- .../Resources/xlf/FrameworkMessages.es.xlf | 4 +- .../Resources/xlf/FrameworkMessages.fr.xlf | 4 +- .../Resources/xlf/FrameworkMessages.it.xlf | 4 +- .../Resources/xlf/FrameworkMessages.ja.xlf | 4 +- .../Resources/xlf/FrameworkMessages.ko.xlf | 4 +- .../Resources/xlf/FrameworkMessages.pl.xlf | 4 +- .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 4 +- .../Resources/xlf/FrameworkMessages.ru.xlf | 4 +- .../Resources/xlf/FrameworkMessages.tr.xlf | 4 +- .../xlf/FrameworkMessages.zh-Hans.xlf | 4 +- .../xlf/FrameworkMessages.zh-Hant.xlf | 4 +- .../Assertions/AssertTests.ScopeTests.cs | 111 ++++++-- .../AssertTests.ThrowsExceptionTests.cs | 6 +- 35 files changed, 513 insertions(+), 174 deletions(-) create mode 100644 docs/RFCs/011-Soft-Assertions-Nullability-Design.md diff --git a/docs/RFCs/011-Soft-Assertions-Nullability-Design.md b/docs/RFCs/011-Soft-Assertions-Nullability-Design.md new file mode 100644 index 0000000000..1b7fc8bc31 --- /dev/null +++ b/docs/RFCs/011-Soft-Assertions-Nullability-Design.md @@ -0,0 +1,263 @@ +# RFC 011 - Soft Assertions and Nullability Annotation Design + +- [x] Approved in principle +- [ ] Under discussion +- [ ] Implementation +- [ ] Shipped + +## Summary + +`Assert.Scope()` introduces soft assertions — assertion failures are collected and reported together when the scope is disposed, rather than throwing immediately. This fundamentally conflicts with C# nullability annotations (`[DoesNotReturn]`, `[DoesNotReturnIf]`, `[NotNull]`) that rely on the assumption that assertion failure always means throwing an exception. This RFC documents the problem, the options considered, and the chosen design. + +## Motivation + +### The soft-assertion goal + +Today, every MSTest assertion throws `AssertFailedException` on failure. With `Assert.Scope()`, we want to allow multiple assertion failures to be collected and reported at once: + +```csharp +using (Assert.Scope()) +{ + Assert.AreEqual(1, actual.X); // failure collected, execution continues + Assert.AreEqual(2, actual.Y); // failure collected, execution continues + Assert.IsTrue(actual.IsValid); // failure collected, execution continues +} +// Dispose() throws AggregateException-like AssertFailedException with all 3 failures +``` + +### The nullability problem + +Before soft assertions, `ThrowAssertFailed` was annotated with `[DoesNotReturn]`, which let the compiler prove post-condition contracts. For example: + +```csharp +public static void IsNotNull([NotNull] object? value, ...) +{ + if (value is null) + { + ThrowAssertIsNotNullFailed(...); // [DoesNotReturn] — compiler trusts value is not null after this + } + // value is known non-null here +} +``` + +To support soft assertions, `ThrowAssertFailed` was changed so it no longer always throws — within a scope, it adds the failure to a queue and *returns*. This means: + +1. `[DoesNotReturn]` can no longer be applied to the general failure path. +2. `[DoesNotReturnIf(false)]` on `IsTrue` / `[DoesNotReturnIf(true)]` on `IsFalse` become lies — the method can return even when the condition is not met. +3. `[NotNull]` on parameters like `IsNotNull(object? value)` becomes a lie — the method can return even when `value` is null. + +If we lie about these annotations, **downstream code after the assertion will get wrong nullability analysis**, potentially causing `NullReferenceException` at runtime with no compiler warning. + +## Options Considered + +### Option 1: Remove all nullability annotations + +Remove `[DoesNotReturn]`, `[DoesNotReturnIf]`, and `[NotNull]` from all assertions. + +**Pros:** Honest to the compiler. No CS8777 warnings. +**Cons:** Massive regression in developer experience. Users who write `Assert.IsNotNull(obj); obj.Method();` would now get a nullable warning on every call after the assertion. This would be a major breaking change to the user experience of the framework. + +**Verdict:** Rejected. Too disruptive for all users, including those who never use `Assert.Scope()`. + +### Option 2: Keep all annotations, suppress all warnings + +Keep `[DoesNotReturn]`, `[DoesNotReturnIf]`, `[NotNull]` on everything, blanket-suppress CS8777/CS8763. + +**Pros:** No user-facing changes. Code compiles cleanly. +**Cons:** The annotations are lies inside a scope. `Assert.IsNotNull(obj)` inside a scope won't throw, meaning `obj` could still be null on the next line, but the compiler thinks it's non-null. This trades a visible assertion failure for a hidden `NullReferenceException`. + +**Verdict:** Rejected. Lying about type-narrowing annotations (`[NotNull]`) is actively dangerous — it causes runtime crashes. + +### Option 3: Pragmatic tier split (chosen) + +Categorize assertions into tiers based on whether their post-conditions narrow types, and handle each tier differently. + +**Tier 1 — Always throw (hard assertions):** Assertions whose annotations change the type state of a variable for subsequent code. These must always throw, even within a scope, because continuing execution with a wrong type assumption would cause immediate downstream errors unrelated to the assertion. + +- `IsNotNull` — annotated `[NotNull]` on the value parameter +- `IsInstanceOfType` — annotated `[NotNull]` on the value parameter +- `IsExactInstanceOfType` — annotated `[NotNull]` on the value parameter +- `Fail` — semantically means "unconditional failure"; annotated `[DoesNotReturn]` on public API +- `ContainsSingle` — returns the matched element; returning `default` in soft mode would give callers a bogus `null`/`default(T)` causing downstream errors + +**Tier 2 — Soft, but annotations removed:** Assertions that had conditional `[DoesNotReturnIf]` annotations. The annotation is removed so the compiler no longer assumes the condition is guaranteed. The assertions become soft (collected within a scope). + +- `IsTrue` — `[DoesNotReturnIf(false)]` removed +- `IsFalse` — `[DoesNotReturnIf(true)]` removed + +**Tier 3 — Soft, no annotation impact:** All other assertions that don't carry type-narrowing annotations. These become fully soft within a scope. + +- `AreEqual`, `AreNotEqual`, `AreSame`, `AreNotSame` +- `Inconclusive` +- `Contains`, `DoesNotContain` +- `IsNull`, `IsNotInstanceOfType`, `IsNotExactInstanceOfType` +- `StartsWith`, `EndsWith`, `Matches`, `DoesNotMatch` +- `IsGreaterThan`, `IsLessThan`, etc. +- `ThrowsException`, `ThrowsExactException` +- All `StringAssert.*` and `CollectionAssert.*` methods + +**Pros:** Type-narrowing contracts are always truthful. Soft assertions work for the vast majority of assertions. The few assertions that must remain hard are exactly the ones where continuing would cause crashes. +**Cons:** `IsNotNull` / `IsInstanceOfType` / `IsExactInstanceOfType` won't participate in soft assertion collection — they still throw immediately within a scope. + +**Verdict:** Chosen. This is the only option that is both honest to the compiler and safe at runtime. + +### Option 3a: Sub-exception for precondition failures + +A variant considered was introducing `internal AssertPreconditionFailedException : AssertFailedException` to distinguish hard failures from soft ones, enabling different handling in the adapter pipeline. + +**Verdict:** Rejected. The existing adapter pipeline checks `is AssertFailedException` in multiple places (`ExceptionExtensions.TryGetUnitTestAssertException`, `TestClassInfo`, `TestMethodInfo`, etc.). A sub-exception would still match these checks. Adding a new exception type adds complexity without clear benefit, and risks breaking extensibility points that pattern-match on `AssertFailedException`. + +## Chosen Design: Two Internal Methods + +### `ReportHardAssertFailure` + +```csharp +[DoesNotReturn] +[StackTraceHidden] +internal static void ReportHardAssertFailure(string assertionName, string? message) +``` + +- **Always throws**, even within an `AssertScope`. +- Carries `[DoesNotReturn]` — compiler can trust post-conditions. +- **Launches the debugger** if configured (`DebuggerLaunchMode.Enabled` / `EnabledExcludingCI`). +- Used by: Tier 1 assertions (`IsNotNull`, `IsInstanceOfType`, `IsExactInstanceOfType`, `Fail`, `ContainsSingle`), `CheckParameterNotNull`, `AssertScope.Dispose()`. + +### `ReportSoftAssertFailure` + +```csharp +[StackTraceHidden] +internal static void ReportSoftAssertFailure(string assertionName, string? message) +``` + +- Within an `AssertScope`: adds failure to the scope's queue and **returns**. +- Outside a scope: **throws** `AssertFailedException` (preserves existing behavior). +- **No `[DoesNotReturn]`** — compiler knows the method can return. +- **Does not launch the debugger** — the debugger is triggered later when `AssertScope.Dispose()` calls `ReportHardAssertFailure`. +- Used by: Tier 2 and Tier 3 assertions. + +### `AssertScope.Dispose()` + +When an `AssertScope` is disposed and it contains collected failures: + +- **Single failure:** Calls `ReportHardAssertFailure(singleError)` — this throws the original `AssertFailedException` and triggers the debugger. +- **Multiple failures:** Calls `ReportHardAssertFailure(new AssertFailedException(combinedMessage, new AggregateException(allErrors)))` — wraps all collected failures into an `AggregateException` as the inner exception. + +This design ensures the debugger breaks at the point where the scope is disposed, giving the developer visibility into all collected failures. + +### `CheckParameterNotNull` + +The internal helper `CheckParameterNotNull` validates that assertion *parameters* (not the values under test) are non-null. For example, validating that a `Type` argument passed to `IsInstanceOfType` is not null. + +This uses `ReportHardAssertFailure` because: + +1. A null parameter is a test authoring bug, not a test value failure. +2. It would be confusing to silently collect a "your parameter was null" error alongside real assertion results. +3. It preserves the existing behavior of throwing `AssertFailedException` (not `ArgumentNullException`), which avoids breaking the adapter pipeline that maps exception types to test outcomes. + +### `Assert.Fail()` — hard by design + +`Assert.Fail()` is a Tier 1 hard assertion. It calls `ReportHardAssertFailure` and always throws, even within a scope. This is the correct choice for two reasons: + +1. **Semantics:** `Fail()` means "this test has unconditionally failed." There is no meaningful scenario where you'd want to collect a `Fail()` and keep executing — the developer explicitly declared the test a failure. +2. **Public API contract:** `Assert.Fail()` is annotated `[DoesNotReturn]`, and users rely on this for control flow: + +```csharp +var result = condition switch +{ + Case.A => HandleA(), + Case.B => HandleB(), + _ => Assert.Fail("Unexpected case") // compiler requires [DoesNotReturn] or it's CS0161 +}; +``` + +Making `Fail()` hard keeps the `[DoesNotReturn]` annotation truthful with no pragma suppression needed. + +## Impact on Users + +### No `Assert.Scope()` — no change + +Users who don't use `Assert.Scope()` experience **zero behavioral change**. All assertions throw exactly as before. The only user-visible annotation change is the removal of `[DoesNotReturnIf]` from `IsTrue`/`IsFalse`, which means the compiler will no longer narrow `bool?` to `bool` after these calls (a minor regression affecting a niche pattern). + +### Within `Assert.Scope()` + +| Assertion | Behavior | +| --------- | -------- | +| `IsNotNull`, `IsInstanceOfType`, `IsExactInstanceOfType` | Always throws immediately (hard). These assertions narrow types and cannot safely be deferred. | +| `Assert.Fail()` | Always throws immediately (hard). Semantically means unconditional failure — no reason to defer. | +| `Assert.ContainsSingle()` | Always throws immediately (hard). Returns the matched element — returning `default` in soft mode would give callers a bogus value. | +| `IsTrue`, `IsFalse` | Soft. Failures collected. `[DoesNotReturnIf]` removed. | +| All other assertions | Soft. Failures collected. | + +## Design Decisions + +### `IsTrue` / `IsFalse` are Tier 2 (soft, annotations removed) + +`IsTrue` had `[DoesNotReturnIf(false)]` and `IsFalse` had `[DoesNotReturnIf(true)]`. These annotations let the compiler narrow `bool?` to `bool` after the call. By making these assertions soft, we had to remove the annotations — the compiler can no longer assume the condition held. + +This was deemed acceptable because: + +- The narrowing only affects `bool?` → `bool`, not reference types. The risk of a downstream `NullReferenceException` does not apply. +- The pattern of using `Assert.IsTrue` to narrow a nullable boolean is niche. Most callers pass a plain `bool`. +- Keeping these as hard assertions would significantly reduce the value of `Assert.Scope()`, since `IsTrue`/`IsFalse` are among the most commonly used assertions. + +This decision can be reconsidered if the annotation loss proves more impactful than expected. + +## Future Improvements + +### Explicit hard-assertion opt-in within a scope + +There may be cases where a user wants a specific assertion to throw immediately within a scope, even though it would normally be soft. A possible API could be: + +```csharp +using (Assert.Scope()) +{ + Assert.AreEqual(1, actual.Count); // soft + Assert.Hard.AreEqual("expected", actual); // hard — throws immediately + Assert.AreEqual(2, actual.Other); // soft +} +``` + +The exact shape of this API is not yet designed. + +### Nested scopes + +Currently, `AssertScope` uses `AsyncLocal` and supports a single active scope. Nested scopes could allow finer-grained grouping of assertion failures: + +```csharp +using (Assert.Scope()) +{ + Assert.AreEqual(1, actual.X); + + using (Assert.Scope()) + { + Assert.AreEqual(2, actual.Y); + Assert.AreEqual(3, actual.Z); + } + // Inner scope disposes here — should inner failures propagate to outer scope or throw? + + Assert.AreEqual(4, actual.W); +} +``` + +The semantics of inner scope disposal (propagate to parent vs. throw immediately) need to be defined. + +### Extensibility for custom assertion authors + +Third-party libraries and users who author custom assertions (via `Assert.That` extension methods or standalone assertion classes) currently have no public API to participate in soft assertion collection. They can only call `Assert.Fail()` (which is hard) or throw `AssertFailedException` directly (which bypasses the scope). + +A future improvement could expose a public API for custom assertion authors to report soft failures, e.g.: + +```csharp +public static class MyCustomAssertions +{ + public static void HasProperty(this Assert assert, object obj, string propertyName) + { + if (obj.GetType().GetProperty(propertyName) is null) + { + Assert.ReportFailure("MyAssert.HasProperty", $"Expected property '{propertyName}' not found."); + } + } +} +``` + +This would require promoting some form of the `ReportSoftAssertFailure` / `ReportHardAssertFailure` API from `internal` to `public`, with careful API design to avoid exposing implementation details. diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 5cd7fe9939..57ec2d5a33 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -642,7 +642,6 @@ private static string FormatStringDifferenceMessage(string expected, string actu new string('-', adjustedCaretPosition) + "^"); } - [DoesNotReturn] private static void ThrowAssertAreEqualFailed(object? expected, object? actual, string userMessage) { string finalMessage = actual != null && expected != null && !actual.GetType().Equals(expected.GetType()) @@ -662,10 +661,9 @@ private static void ThrowAssertAreEqualFailed(object? expected, object? actual, userMessage, ReplaceNulls(expected), ReplaceNulls(actual)); - ThrowAssertFailed("Assert.AreEqual", finalMessage); + ReportSoftAssertFailure("Assert.AreEqual", finalMessage); } - [DoesNotReturn] private static void ThrowAssertAreEqualFailed(T expected, T actual, T delta, string userMessage) where T : struct, IConvertible { @@ -676,10 +674,9 @@ private static void ThrowAssertAreEqualFailed(T expected, T actual, T delta, expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat)); - ThrowAssertFailed("Assert.AreEqual", finalMessage); + ReportSoftAssertFailure("Assert.AreEqual", finalMessage); } - [DoesNotReturn] private static void ThrowAssertAreEqualFailed(string? expected, string? actual, bool ignoreCase, CultureInfo culture, string userMessage) { string finalMessage; @@ -700,7 +697,7 @@ private static void ThrowAssertAreEqualFailed(string? expected, string? actual, finalMessage = FormatStringComparisonMessage(expected, actual, userMessage); } - ThrowAssertFailed("Assert.AreEqual", finalMessage); + ReportSoftAssertFailure("Assert.AreEqual", finalMessage); } /// @@ -1218,7 +1215,6 @@ private static bool AreNotEqualFailing(double notExpected, double actual, double return Math.Abs(notExpected - actual) <= delta; } - [DoesNotReturn] private static void ThrowAssertAreNotEqualFailed(T notExpected, T actual, T delta, string userMessage) where T : struct, IConvertible { @@ -1229,7 +1225,7 @@ private static void ThrowAssertAreNotEqualFailed(T notExpected, T actual, T d notExpected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat)); - ThrowAssertFailed("Assert.AreNotEqual", finalMessage); + ReportSoftAssertFailure("Assert.AreNotEqual", finalMessage); } /// @@ -1428,7 +1424,6 @@ private static bool AreNotEqualFailing(string? notExpected, string? actual, bool private static bool AreNotEqualFailing(T? notExpected, T? actual, IEqualityComparer? comparer) => (comparer ?? EqualityComparer.Default).Equals(notExpected!, actual!); - [DoesNotReturn] private static void ThrowAssertAreNotEqualFailed(object? notExpected, object? actual, string userMessage) { string finalMessage = string.Format( @@ -1437,7 +1432,7 @@ private static void ThrowAssertAreNotEqualFailed(object? notExpected, object? ac userMessage, ReplaceNulls(notExpected), ReplaceNulls(actual)); - ThrowAssertFailed("Assert.AreNotEqual", finalMessage); + ReportSoftAssertFailure("Assert.AreNotEqual", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs index 93be91c7fd..468c7eba9b 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs @@ -184,7 +184,6 @@ public static void AreSame(T? expected, T? actual, string? message = "", [Cal private static bool IsAreSameFailing(T? expected, T? actual) => !object.ReferenceEquals(expected, actual); - [DoesNotReturn] private static void ThrowAssertAreSameFailed(T? expected, T? actual, string userMessage) { string finalMessage = userMessage; @@ -196,7 +195,7 @@ private static void ThrowAssertAreSameFailed(T? expected, T? actual, string u userMessage); } - ThrowAssertFailed("Assert.AreSame", finalMessage); + ReportSoftAssertFailure("Assert.AreSame", finalMessage); } /// @@ -247,7 +246,6 @@ public static void AreNotSame(T? notExpected, T? actual, string? message = "" private static bool IsAreNotSameFailing(T? notExpected, T? actual) => object.ReferenceEquals(notExpected, actual); - [DoesNotReturn] private static void ThrowAssertAreNotSameFailed(string userMessage) - => ThrowAssertFailed("Assert.AreNotSame", userMessage); + => ReportSoftAssertFailure("Assert.AreNotSame", userMessage); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs index 819452d1f4..cae66021ca 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs @@ -478,7 +478,7 @@ public static void Contains(string substring, string value, StringComparison com { string userMessage = BuildUserMessageForSubstringExpressionAndValueExpression(message, substringExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.ContainsFail, value, substring, userMessage); - ThrowAssertFailed("Assert.Contains", finalMessage); + ReportSoftAssertFailure("Assert.Contains", finalMessage); } } @@ -717,7 +717,7 @@ public static void DoesNotContain(string substring, string value, StringComparis { string userMessage = BuildUserMessageForSubstringExpressionAndValueExpression(message, substringExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainFail, value, substring, userMessage); - ThrowAssertFailed("Assert.DoesNotContain", finalMessage); + ReportSoftAssertFailure("Assert.DoesNotContain", finalMessage); } } @@ -758,7 +758,7 @@ public static void IsInRange(T minValue, T maxValue, T value, string? message { string userMessage = BuildUserMessageForMinValueExpressionAndMaxValueExpressionAndValueExpression(message, minValueExpression, maxValueExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsInRangeFail, value, minValue, maxValue, userMessage); - ThrowAssertFailed("IsInRange", finalMessage); + ReportSoftAssertFailure("IsInRange", finalMessage); } } @@ -772,7 +772,7 @@ private static void ThrowAssertSingleMatchFailed(int actualCount, string userMes FrameworkMessages.ContainsSingleMatchFailMsg, userMessage, actualCount); - ThrowAssertFailed("Assert.ContainsSingle", finalMessage); + ReportHardAssertFailure("Assert.ContainsSingle", finalMessage); } [DoesNotReturn] @@ -783,46 +783,42 @@ private static void ThrowAssertContainsSingleFailed(int actualCount, string user FrameworkMessages.ContainsSingleFailMsg, userMessage, actualCount); - ThrowAssertFailed("Assert.ContainsSingle", finalMessage); + ReportHardAssertFailure("Assert.ContainsSingle", finalMessage); } - [DoesNotReturn] private static void ThrowAssertContainsItemFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.ContainsItemFailMsg, userMessage); - ThrowAssertFailed("Assert.Contains", finalMessage); + ReportSoftAssertFailure("Assert.Contains", finalMessage); } - [DoesNotReturn] private static void ThrowAssertContainsPredicateFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.ContainsPredicateFailMsg, userMessage); - ThrowAssertFailed("Assert.Contains", finalMessage); + ReportSoftAssertFailure("Assert.Contains", finalMessage); } - [DoesNotReturn] private static void ThrowAssertDoesNotContainItemFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainItemFailMsg, userMessage); - ThrowAssertFailed("Assert.DoesNotContain", finalMessage); + ReportSoftAssertFailure("Assert.DoesNotContain", finalMessage); } - [DoesNotReturn] private static void ThrowAssertDoesNotContainPredicateFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainPredicateFailMsg, userMessage); - ThrowAssertFailed("Assert.DoesNotContain", finalMessage); + ReportSoftAssertFailure("Assert.DoesNotContain", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs index 555d341686..5940b0f8b7 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs @@ -333,7 +333,6 @@ private static void HasCount(string assertionName, int expected, IEnumerable< private static void HasCount(string assertionName, int expected, IEnumerable collection, string? message, string collectionExpression) => HasCount(assertionName, expected, collection.Cast(), message, collectionExpression); - [DoesNotReturn] private static void ThrowAssertCountFailed(string assertionName, int expectedCount, int actualCount, string userMessage) { string finalMessage = string.Format( @@ -342,16 +341,15 @@ private static void ThrowAssertCountFailed(string assertionName, int expectedCou userMessage, expectedCount, actualCount); - ThrowAssertFailed($"Assert.{assertionName}", finalMessage); + ReportSoftAssertFailure($"Assert.{assertionName}", finalMessage); } - [DoesNotReturn] private static void ThrowAssertIsNotEmptyFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.IsNotEmptyFailMsg, userMessage); - ThrowAssertFailed("Assert.IsNotEmpty", finalMessage); + ReportSoftAssertFailure("Assert.IsNotEmpty", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs b/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs index 772c1d1e48..ca9ca27605 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs @@ -78,7 +78,7 @@ public static void EndsWith([NotNull] string? expectedSuffix, [NotNull] string? { string userMessage = BuildUserMessageForExpectedSuffixExpressionAndValueExpression(message, expectedSuffixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.EndsWithFail, value, expectedSuffix, userMessage); - ThrowAssertFailed("Assert.EndsWith", finalMessage); + ReportSoftAssertFailure("Assert.EndsWith", finalMessage); } } @@ -152,7 +152,7 @@ public static void DoesNotEndWith([NotNull] string? notExpectedSuffix, [NotNull] { string userMessage = BuildUserMessageForNotExpectedSuffixExpressionAndValueExpression(message, notExpectedSuffixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotEndWithFail, value, notExpectedSuffix, userMessage); - ThrowAssertFailed("Assert.DoesNotEndWith", finalMessage); + ReportSoftAssertFailure("Assert.DoesNotEndWith", finalMessage); } } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs b/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs index 3a3a4ebd24..ce60828296 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs @@ -22,5 +22,5 @@ public sealed partial class Assert /// [DoesNotReturn] public static void Fail(string message = "") - => ThrowAssertFailed("Assert.Fail", BuildUserMessage(message)); + => ReportHardAssertFailure("Assert.Fail", BuildUserMessage(message)); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs b/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs index a953f5cd33..86b1b217b1 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs @@ -298,7 +298,6 @@ public static void IsNegative(T value, string? message = "", [CallerArgumentE #endregion // IsNegative - [DoesNotReturn] private static void ThrowAssertIsGreaterThanFailed(T lowerBound, T value, string userMessage) { string finalMessage = string.Format( @@ -307,10 +306,9 @@ private static void ThrowAssertIsGreaterThanFailed(T lowerBound, T value, str userMessage, ReplaceNulls(lowerBound), ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsGreaterThan", finalMessage); + ReportSoftAssertFailure("Assert.IsGreaterThan", finalMessage); } - [DoesNotReturn] private static void ThrowAssertIsGreaterThanOrEqualToFailed(T lowerBound, T value, string userMessage) { string finalMessage = string.Format( @@ -319,10 +317,9 @@ private static void ThrowAssertIsGreaterThanOrEqualToFailed(T lowerBound, T v userMessage, ReplaceNulls(lowerBound), ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsGreaterThanOrEqualTo", finalMessage); + ReportSoftAssertFailure("Assert.IsGreaterThanOrEqualTo", finalMessage); } - [DoesNotReturn] private static void ThrowAssertIsLessThanFailed(T upperBound, T value, string userMessage) { string finalMessage = string.Format( @@ -331,10 +328,9 @@ private static void ThrowAssertIsLessThanFailed(T upperBound, T value, string userMessage, ReplaceNulls(upperBound), ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsLessThan", finalMessage); + ReportSoftAssertFailure("Assert.IsLessThan", finalMessage); } - [DoesNotReturn] private static void ThrowAssertIsLessThanOrEqualToFailed(T upperBound, T value, string userMessage) { string finalMessage = string.Format( @@ -343,10 +339,9 @@ private static void ThrowAssertIsLessThanOrEqualToFailed(T upperBound, T valu userMessage, ReplaceNulls(upperBound), ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsLessThanOrEqualTo", finalMessage); + ReportSoftAssertFailure("Assert.IsLessThanOrEqualTo", finalMessage); } - [DoesNotReturn] private static void ThrowAssertIsPositiveFailed(T value, string userMessage) { string finalMessage = string.Format( @@ -354,10 +349,9 @@ private static void ThrowAssertIsPositiveFailed(T value, string userMessage) FrameworkMessages.IsPositiveFailMsg, userMessage, ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsPositive", finalMessage); + ReportSoftAssertFailure("Assert.IsPositive", finalMessage); } - [DoesNotReturn] private static void ThrowAssertIsNegativeFailed(T value, string userMessage) { string finalMessage = string.Format( @@ -365,6 +359,6 @@ private static void ThrowAssertIsNegativeFailed(T value, string userMessage) FrameworkMessages.IsNegativeFailMsg, userMessage, ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsNegative", finalMessage); + ReportSoftAssertFailure("Assert.IsNegative", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index 144f17cdbe..b5260eefe5 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -342,7 +342,7 @@ private static void ThrowAssertIsExactInstanceOfTypeFailed(object? value, Type? value.GetType().ToString()); } - ThrowAssertFailed("Assert.IsExactInstanceOfType", finalMessage); + ReportHardAssertFailure("Assert.IsExactInstanceOfType", finalMessage); } /// @@ -369,7 +369,7 @@ private static void ThrowAssertIsExactInstanceOfTypeFailed(object? value, Type? /// is exactly the type /// of . /// - public static void IsNotExactInstanceOfType(object? value, [NotNull] Type? wrongType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") + public static void IsNotExactInstanceOfType(object? value, Type? wrongType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsNotExactInstanceOfTypeFailing(value, wrongType)) { @@ -379,11 +379,9 @@ public static void IsNotExactInstanceOfType(object? value, [NotNull] Type? wrong /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 - public static void IsNotExactInstanceOfType(object? value, [NotNull] Type? wrongType, [InterpolatedStringHandlerArgument(nameof(value), nameof(wrongType))] ref AssertIsNotExactInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") + public static void IsNotExactInstanceOfType(object? value, Type? wrongType, [InterpolatedStringHandlerArgument(nameof(value), nameof(wrongType))] ref AssertIsNotExactInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. => message.ComputeAssertion(valueExpression); -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// /// Tests whether the specified object is not exactly an instance of the wrong generic @@ -404,7 +402,6 @@ private static bool IsNotExactInstanceOfTypeFailing(object? value, [NotNullWhen( // Null is not an instance of any type. (value is not null && value.GetType() == wrongType); - [DoesNotReturn] private static void ThrowAssertIsNotExactInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) { string finalMessage = userMessage; @@ -418,6 +415,6 @@ private static void ThrowAssertIsNotExactInstanceOfTypeFailed(object? value, Typ value!.GetType().ToString()); } - ThrowAssertFailed("Assert.IsNotExactInstanceOfType", finalMessage); + ReportSoftAssertFailure("Assert.IsNotExactInstanceOfType", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index bdd68f1607..cef8c6e26a 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -344,7 +344,7 @@ private static void ThrowAssertIsInstanceOfTypeFailed(object? value, Type? expec value.GetType().ToString()); } - ThrowAssertFailed("Assert.IsInstanceOfType", finalMessage); + ReportHardAssertFailure("Assert.IsInstanceOfType", finalMessage); } /// @@ -372,7 +372,7 @@ private static void ThrowAssertIsInstanceOfTypeFailed(object? value, Type? expec /// is in the inheritance hierarchy /// of . /// - public static void IsNotInstanceOfType(object? value, [NotNull] Type? wrongType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") + public static void IsNotInstanceOfType(object? value, Type? wrongType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsNotInstanceOfTypeFailing(value, wrongType)) { @@ -382,11 +382,9 @@ public static void IsNotInstanceOfType(object? value, [NotNull] Type? wrongType, /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 - public static void IsNotInstanceOfType(object? value, [NotNull] Type? wrongType, [InterpolatedStringHandlerArgument(nameof(value), nameof(wrongType))] ref AssertIsNotInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") + public static void IsNotInstanceOfType(object? value, Type? wrongType, [InterpolatedStringHandlerArgument(nameof(value), nameof(wrongType))] ref AssertIsNotInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. => message.ComputeAssertion(valueExpression); -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// /// Tests whether the specified object is not an instance of the wrong generic @@ -408,7 +406,6 @@ private static bool IsNotInstanceOfTypeFailing(object? value, [NotNullWhen(false // Null is not an instance of any type. (value is not null && wrongType.IsInstanceOfType(value)); - [DoesNotReturn] private static void ThrowAssertIsNotInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) { string finalMessage = userMessage; @@ -422,6 +419,6 @@ private static void ThrowAssertIsNotInstanceOfTypeFailed(object? value, Type? wr value!.GetType().ToString()); } - ThrowAssertFailed("Assert.IsNotInstanceOfType", finalMessage); + ReportSoftAssertFailure("Assert.IsNotInstanceOfType", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index fd6db5b3d4..a26d6826df 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -159,7 +159,7 @@ public static void IsNull(object? value, string? message = "", [CallerArgumentEx private static bool IsNullFailing(object? value) => value is not null; private static void ThrowAssertIsNullFailed(string? message) - => ThrowAssertFailed("Assert.IsNull", message); + => ReportSoftAssertFailure("Assert.IsNull", message); /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 @@ -199,5 +199,5 @@ public static void IsNotNull([NotNull] object? value, string? message = "", [Cal [DoesNotReturn] private static void ThrowAssertIsNotNullFailed(string? message) - => ThrowAssertFailed("Assert.IsNotNull", message); + => ReportHardAssertFailure("Assert.IsNotNull", message); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs index 1c31f3add1..344a91d2d2 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs @@ -124,7 +124,7 @@ internal void ComputeAssertion(string conditionExpression) /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 - public static void IsTrue([DoesNotReturnIf(false)] bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsTrueInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") + public static void IsTrue(bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsTrueInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") #pragma warning restore IDE0060 // Remove unused parameter => message.ComputeAssertion(conditionExpression); @@ -146,7 +146,7 @@ public static void IsTrue([DoesNotReturnIf(false)] bool? condition, [Interpolate /// /// Thrown if is false. /// - public static void IsTrue([DoesNotReturnIf(false)] bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") + public static void IsTrue(bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") { if (IsTrueFailing(condition)) { @@ -158,11 +158,11 @@ private static bool IsTrueFailing(bool? condition) => condition is false or null; private static void ThrowAssertIsTrueFailed(string? message) - => ThrowAssertFailed("Assert.IsTrue", message); + => ReportSoftAssertFailure("Assert.IsTrue", message); /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 - public static void IsFalse([DoesNotReturnIf(true)] bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsFalseInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") + public static void IsFalse(bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsFalseInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") #pragma warning restore IDE0060 // Remove unused parameter => message.ComputeAssertion(conditionExpression); @@ -184,7 +184,7 @@ public static void IsFalse([DoesNotReturnIf(true)] bool? condition, [Interpolate /// /// Thrown if is true. /// - public static void IsFalse([DoesNotReturnIf(true)] bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") + public static void IsFalse(bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") { if (IsFalseFailing(condition)) { @@ -195,7 +195,6 @@ public static void IsFalse([DoesNotReturnIf(true)] bool? condition, string? mess private static bool IsFalseFailing(bool? condition) => condition is true or null; - [DoesNotReturn] private static void ThrowAssertIsFalseFailed(string userMessage) - => ThrowAssertFailed("Assert.IsFalse", userMessage); + => ReportSoftAssertFailure("Assert.IsFalse", userMessage); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs b/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs index b204970b6d..0fd3cb54af 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs @@ -46,7 +46,7 @@ public static void MatchesRegex([NotNull] Regex? pattern, [NotNull] string? valu { string userMessage = BuildUserMessageForPatternExpressionAndValueExpression(message, patternExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsMatchFail, value, pattern, userMessage); - ThrowAssertFailed("Assert.MatchesRegex", finalMessage); + ReportSoftAssertFailure("Assert.MatchesRegex", finalMessage); } } @@ -122,7 +122,7 @@ public static void DoesNotMatchRegex([NotNull] Regex? pattern, [NotNull] string? { string userMessage = BuildUserMessageForPatternExpressionAndValueExpression(message, patternExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsNotMatchFail, value, pattern, userMessage); - ThrowAssertFailed("Assert.DoesNotMatchRegex", finalMessage); + ReportSoftAssertFailure("Assert.DoesNotMatchRegex", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs b/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs index 6f5c728535..cd10404326 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs @@ -78,7 +78,7 @@ public static void StartsWith([NotNull] string? expectedPrefix, [NotNull] string { string userMessage = BuildUserMessageForExpectedPrefixExpressionAndValueExpression(message, expectedPrefixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.StartsWithFail, value, expectedPrefix, userMessage); - ThrowAssertFailed("Assert.StartsWith", finalMessage); + ReportSoftAssertFailure("Assert.StartsWith", finalMessage); } } @@ -150,7 +150,7 @@ public static void DoesNotStartWith([NotNull] string? notExpectedPrefix, [NotNul { string userMessage = BuildUserMessageForNotExpectedPrefixExpressionAndValueExpression(message, notExpectedPrefixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotStartWithFail, value, notExpectedPrefix, userMessage); - ThrowAssertFailed("Assert.DoesNotStartWith", finalMessage); + ReportSoftAssertFailure("Assert.DoesNotStartWith", finalMessage); } } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs index b5478a28a6..1ad0653768 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs @@ -538,7 +538,7 @@ private static async Task IsThrowsAsyncFailingAsync IsThrowsAsyncFailingAsync(Action action, b userMessage, typeof(TException), ex.GetType()); - ThrowAssertFailed("Assert." + assertMethodName, finalMessage); + ReportSoftAssertFailure("Assert." + assertMethodName, finalMessage); }, ex); } @@ -590,7 +590,7 @@ private static ThrowsExceptionState IsThrowsFailing(Action action, b FrameworkMessages.NoExceptionThrown, userMessage, typeof(TException)); - ThrowAssertFailed("Assert." + assertMethodName, finalMessage); + ReportSoftAssertFailure("Assert." + assertMethodName, finalMessage); }, null); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 8db7db534c..78def33fa7 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -26,7 +26,10 @@ private Assert() public static Assert That { get; } = new(); /// - /// Helper function that creates and throws an AssertionFailedException. + /// Reports a hard assertion failure. This always throws immediately, even within an , + /// and triggers the debugger if configured. Use for assertions whose annotations change the + /// type of a variable (e.g., [NotNull]) where continuing execution would cause downstream errors, + /// and for precondition violations (e.g., a required parameter is ). /// /// /// name of the assertion throwing an exception. @@ -36,7 +39,52 @@ private Assert() /// [DoesNotReturn] [StackTraceHidden] - internal static void ThrowAssertFailed(string assertionName, string? message) + internal static void ReportHardAssertFailure(string assertionName, string? message) + => ReportHardAssertFailure(new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message))); + + /// + /// Reports a hard assertion failure using a pre-built exception. This always throws immediately, + /// even within an , and triggers the debugger if configured. + /// + /// + /// The assertion failure exception to throw. + /// + [DoesNotReturn] + [StackTraceHidden] + internal static void ReportHardAssertFailure(AssertFailedException exception) + { + LaunchDebuggerIfNeeded(); + throw exception; + } + + /// + /// Reports a soft assertion failure. Within an , the failure is collected + /// and execution continues. Outside a scope, the failure is thrown immediately. + /// Does not trigger the debugger — the debugger is triggered when the scope is disposed + /// and the collected failures are reported. + /// + /// + /// name of the assertion throwing an exception. + /// + /// + /// The assertion failure message. + /// + [StackTraceHidden] + internal static void ReportSoftAssertFailure(string assertionName, string? message) + { + var assertionFailedException = new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); + AssertScope? scope = AssertScope.Current; + if (scope is not null) + { + scope.AddError(assertionFailedException); + return; + } + + throw assertionFailedException; + } + + [StackTraceHidden] + private static void LaunchDebuggerIfNeeded() { if (ShouldLaunchDebugger()) { @@ -49,18 +97,6 @@ internal static void ThrowAssertFailed(string assertionName, string? message) Debugger.Launch(); } } - - var assertionFailedException = new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); - AssertScope? scope = AssertScope.Current; - if (scope is not null) - { - scope.AddError(assertionFailedException); -#pragma warning disable CS8763 // A method marked [DoesNotReturn] should not return. - return; -#pragma warning restore CS8763 // A method marked [DoesNotReturn] should not return. - } - - throw assertionFailedException; } private static bool ShouldLaunchDebugger() @@ -193,10 +229,10 @@ private static string BuildUserMessageForMinValueExpressionAndMaxValueExpression /// internal static void CheckParameterNotNull([NotNull] object? param, string assertionName, string parameterName) { - if (param == null) + if (param is null) { string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.NullParameterToAssert, parameterName); - ThrowAssertFailed(assertionName, finalMessage); + ReportHardAssertFailure(assertionName, finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs index 986dd3c84a..5b467b8b5d 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertScope.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -58,14 +58,15 @@ public void Dispose() if (_errors.Count == 1 && _errors.TryDequeue(out AssertFailedException? singleError)) { - throw singleError; + Assert.ReportHardAssertFailure(singleError); } if (!_errors.IsEmpty) { - throw new AssertFailedException( - string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), - new AggregateException(_errors)); + Assert.ReportHardAssertFailure( + new AssertFailedException( + string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), + new AggregateException(_errors))); } } } diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs index 6be196fa94..aef4d1c031 100644 --- a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs @@ -79,7 +79,7 @@ public static void Contains([NotNull] ICollection? collection, object? element, } } - Assert.ThrowAssertFailed("CollectionAssert.Contains", Assert.BuildUserMessage(message)); + Assert.ReportSoftAssertFailure("CollectionAssert.Contains", Assert.BuildUserMessage(message)); } /// @@ -126,7 +126,7 @@ public static void DoesNotContain([NotNull] ICollection? collection, object? ele { if (object.Equals(current, element)) { - Assert.ThrowAssertFailed("CollectionAssert.DoesNotContain", Assert.BuildUserMessage(message)); + Assert.ReportSoftAssertFailure("CollectionAssert.DoesNotContain", Assert.BuildUserMessage(message)); } } } @@ -165,7 +165,7 @@ public static void AllItemsAreNotNull([NotNull] ICollection? collection, string? { if (current == null) { - Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreNotNull", Assert.BuildUserMessage(message)); + Assert.ReportSoftAssertFailure("CollectionAssert.AllItemsAreNotNull", Assert.BuildUserMessage(message)); } } } @@ -226,7 +226,7 @@ public static void AllItemsAreUnique([NotNull] ICollection? collection, string? userMessage, FrameworkMessages.Common_NullInMessages); - Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AllItemsAreUnique", finalMessage); } } else @@ -240,7 +240,7 @@ public static void AllItemsAreUnique([NotNull] ICollection? collection, string? userMessage, Assert.ReplaceNulls(current)); - Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AllItemsAreUnique", finalMessage); } } } @@ -303,11 +303,11 @@ public static void IsSubsetOf([NotNull] ICollection? subset, [NotNull] ICollecti string userMessage = Assert.BuildUserMessage(message); if (string.IsNullOrEmpty(userMessage)) { - Assert.ThrowAssertFailed("CollectionAssert.IsSubsetOf", returnedSubsetValueMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.IsSubsetOf", returnedSubsetValueMessage); } else { - Assert.ThrowAssertFailed("CollectionAssert.IsSubsetOf", $"{returnedSubsetValueMessage} {userMessage}"); + Assert.ReportSoftAssertFailure("CollectionAssert.IsSubsetOf", $"{returnedSubsetValueMessage} {userMessage}"); } } } @@ -357,7 +357,7 @@ public static void IsNotSubsetOf([NotNull] ICollection? subset, [NotNull] IColle Tuple> isSubsetValue = IsSubsetOfHelper(subset, superset); if (isSubsetValue.Item1) { - Assert.ThrowAssertFailed("CollectionAssert.IsNotSubsetOf", Assert.BuildUserMessage(message)); + Assert.ReportSoftAssertFailure("CollectionAssert.IsNotSubsetOf", Assert.BuildUserMessage(message)); } } @@ -476,7 +476,7 @@ public static void AreEquivalent( // Check whether one is null while the other is not. if (expected == null != (actual == null)) { - Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", Assert.BuildUserMessage(message)); + Assert.ReportSoftAssertFailure("CollectionAssert.AreEquivalent", Assert.BuildUserMessage(message)); } // If the references are the same or both collections are null, they are equivalent. @@ -500,7 +500,7 @@ public static void AreEquivalent( userMessage, expectedCollectionCount, actualCollectionCount); - Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreEquivalent", finalMessage); } // If both collections are empty, they are equivalent. @@ -520,7 +520,7 @@ public static void AreEquivalent( expectedCount.ToString(CultureInfo.CurrentCulture.NumberFormat), Assert.ReplaceNulls(mismatchedElement), actualCount.ToString(CultureInfo.CurrentCulture.NumberFormat)); - Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreEquivalent", finalMessage); } // All the elements and counts matched. @@ -654,7 +654,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothCollectionsSameReference, userMessage); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEquivalent", finalMessage); } DebugEx.Assert(actual is not null, "actual is not null here"); @@ -674,7 +674,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothCollectionsEmpty, userMessage); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEquivalent", finalMessage); } // Search for a mismatched element. @@ -685,7 +685,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothSameElements, userMessage); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEquivalent", finalMessage); } } @@ -755,7 +755,7 @@ public static void AllItemsAreInstancesOfType( i, expectedType.ToString(), element.GetType().ToString()); - Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreInstancesOfType", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AllItemsAreInstancesOfType", finalMessage); } i++; @@ -816,7 +816,7 @@ public static void AreEqual(ICollection? expected, ICollection? actual, string? if (!AreCollectionsEqual(expected, actual, new ObjectComparer(), ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ThrowAssertFailed("CollectionAssert.AreEqual", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreEqual", finalMessage); } } @@ -870,7 +870,7 @@ public static void AreNotEqual(ICollection? notExpected, ICollection? actual, st if (AreCollectionsEqual(notExpected, actual, new ObjectComparer(), ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEqual", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEqual", finalMessage); } } @@ -928,7 +928,7 @@ public static void AreEqual(ICollection? expected, ICollection? actual, [NotNull if (!AreCollectionsEqual(expected, actual, comparer, ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ThrowAssertFailed("CollectionAssert.AreEqual", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreEqual", finalMessage); } } @@ -986,7 +986,7 @@ public static void AreNotEqual(ICollection? notExpected, ICollection? actual, [N if (AreCollectionsEqual(notExpected, actual, comparer, ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEqual", finalMessage); + Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEqual", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/StringAssert.cs b/src/TestFramework/TestFramework/Assertions/StringAssert.cs index b2e36625ec..69037de0dd 100644 --- a/src/TestFramework/TestFramework/Assertions/StringAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/StringAssert.cs @@ -122,7 +122,7 @@ public static void Contains([NotNull] string? value, [NotNull] string? substring { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.ContainsFail, value, substring, userMessage); - Assert.ThrowAssertFailed("StringAssert.Contains", finalMessage); + Assert.ReportSoftAssertFailure("StringAssert.Contains", finalMessage); } } @@ -219,7 +219,7 @@ public static void StartsWith([NotNull] string? value, [NotNull] string? substri { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.StartsWithFail, value, substring, userMessage); - Assert.ThrowAssertFailed("StringAssert.StartsWith", finalMessage); + Assert.ReportSoftAssertFailure("StringAssert.StartsWith", finalMessage); } } @@ -316,7 +316,7 @@ public static void EndsWith([NotNull] string? value, [NotNull] string? substring { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.EndsWithFail, value, substring, userMessage); - Assert.ThrowAssertFailed("StringAssert.EndsWith", finalMessage); + Assert.ReportSoftAssertFailure("StringAssert.EndsWith", finalMessage); } } @@ -371,7 +371,7 @@ public static void Matches([NotNull] string? value, [NotNull] Regex? pattern, st { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsMatchFail, value, pattern, userMessage); - Assert.ThrowAssertFailed("StringAssert.Matches", finalMessage); + Assert.ReportSoftAssertFailure("StringAssert.Matches", finalMessage); } } @@ -422,7 +422,7 @@ public static void DoesNotMatch([NotNull] string? value, [NotNull] Regex? patter { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsNotMatchFail, value, pattern, userMessage); - Assert.ThrowAssertFailed("StringAssert.DoesNotMatch", finalMessage); + Assert.ReportSoftAssertFailure("StringAssert.DoesNotMatch", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index bf8802e3ee..995b5b836a 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -394,7 +394,7 @@ Actual: {2} The maximum value must be greater than or equal to the minimum value. - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index 31747db419..d6c8988178 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -459,8 +459,8 @@ Skutečnost: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index ac053b5a13..06c1678819 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -459,8 +459,8 @@ Tatsächlich: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 48f70b8219..e83482bce6 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -459,8 +459,8 @@ Real: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 246b8a2447..b215f7b50e 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -459,8 +459,8 @@ Réel : {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index ef6a99d45c..420b48118d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -459,8 +459,8 @@ Effettivo: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index d8bd32459b..c6bd67b833 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -459,8 +459,8 @@ Actual: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index 4bf2938b50..761092be62 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -459,8 +459,8 @@ Actual: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 64e08aac69..5698f2ff34 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -459,8 +459,8 @@ Rzeczywiste: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index 3bf27131b9..b509b4cabf 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -459,8 +459,8 @@ Real: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 118a2af866..682759bdd7 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -459,8 +459,8 @@ Actual: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 61bb9cc180..1622a38428 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -459,8 +459,8 @@ Gerçekte olan: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 32d51b44ac..7f0070d43c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -459,8 +459,8 @@ Actual: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 652d0fb46f..83cb88277f 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -459,8 +459,8 @@ Actual: {2} - {0} assertion(s) failed within the assert scope: - {0} assertion(s) failed within the assert scope: + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. {0} is the number of assertion failures collected in the scope. diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index 8bfa3d994d..c166ddfb05 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -32,7 +32,7 @@ public void Scope_SingleFailure_ThrowsOnDispose() }; action.Should().Throw() - .WithMessage("*Assert.AreEqual failed*"); + .WithMessage("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); } public void Scope_MultipleFailures_CollectsAllErrors() @@ -46,9 +46,14 @@ public void Scope_MultipleFailures_CollectsAllErrors() } }; - action.Should().Throw() - .And.Message.Should().Contain("Assert.AreEqual failed") - .And.Contain("Assert.IsTrue failed"); + AggregateException innerException = action.Should().Throw() + .WithMessage("2 assertion(s) failed within the assert scope.") + .WithInnerException() + .Which; + + innerException.InnerExceptions.Should().HaveCount(2); + innerException.InnerExceptions[0].Message.Should().Be("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + innerException.InnerExceptions[1].Message.Should().Be("Assert.IsTrue failed. 'condition' expression: 'false'."); } public void Scope_AfterDispose_AssertionsThrowNormally() @@ -70,30 +75,21 @@ public void Scope_AfterDispose_AssertionsThrowNormally() action.Should().Throw(); } - public void Scope_NestedScopes_InnerScopeCollectsItsOwnErrors() + public void Scope_NestedScope_ThrowsInvalidOperationException() { Action action = () => { using (Assert.Scope()) { - Assert.AreEqual(1, 2); // outer error - - Action innerAction = () => + using (Assert.Scope()) { - using (Assert.Scope()) - { - Assert.IsTrue(false); // inner error - } - }; - - innerAction.Should().Throw() - .WithMessage("*Assert.IsTrue failed*"); + // Should never reach here + } } }; - // Outer scope should only contain the outer error - action.Should().Throw() - .And.Message.Should().Contain("Assert.AreEqual failed"); + action.Should().Throw() + .WithMessage("Nested assert scopes are not allowed. Dispose the current scope before creating a new one."); } public void Scope_DoubleDispose_DoesNotThrowTwice() @@ -102,14 +98,15 @@ public void Scope_DoubleDispose_DoesNotThrowTwice() Assert.AreEqual(1, 2); Action firstDispose = () => scope.Dispose(); - firstDispose.Should().Throw(); + firstDispose.Should().Throw() + .WithMessage("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); // Second dispose should be a no-op Action secondDispose = () => scope.Dispose(); secondDispose.Should().NotThrow(); } - public void Scope_AssertFail_IsCollected() + public void Scope_AssertFail_IsHardFailure() { Action action = () => { @@ -120,8 +117,76 @@ public void Scope_AssertFail_IsCollected() } }; + // Assert.Fail is a hard assertion — it throws immediately, even within a scope. + action.Should().Throw() + .WithMessage("Assert.Fail failed. first failure"); + } + + public void Scope_AssertIsNotNull_IsHardFailure() + { + object? value = null; + Action action = () => + { + using (Assert.Scope()) + { + Assert.IsNotNull(value); + Assert.IsTrue(true); // should not be reached + } + }; + + // Assert.IsNotNull is a hard assertion — it throws immediately, even within a scope. + action.Should().Throw() + .WithMessage("Assert.IsNotNull failed. 'value' expression: 'value'."); + } + + public void Scope_AssertIsInstanceOfType_IsHardFailure() + { + object value = "hello"; + Action action = () => + { + using (Assert.Scope()) + { + Assert.IsInstanceOfType(value, typeof(int)); + Assert.IsTrue(true); // should not be reached + } + }; + + // Assert.IsInstanceOfType is a hard assertion — it throws immediately, even within a scope. + action.Should().Throw() + .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: 'value'. Expected type:. Actual type:."); + } + + public void Scope_AssertIsExactInstanceOfType_IsHardFailure() + { + object value = "hello"; + Action action = () => + { + using (Assert.Scope()) + { + Assert.IsExactInstanceOfType(value, typeof(object)); + Assert.IsTrue(true); // should not be reached + } + }; + + // Assert.IsExactInstanceOfType is a hard assertion — it throws immediately, even within a scope. + action.Should().Throw() + .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: 'value'. Expected exact type:. Actual type:."); + } + + public void Scope_AssertContainsSingle_IsHardFailure() + { + int[] items = [1, 2, 3]; + Action action = () => + { + using (Assert.Scope()) + { + Assert.ContainsSingle(items); + Assert.IsTrue(true); // should not be reached + } + }; + + // Assert.ContainsSingle is a hard assertion — it throws immediately, even within a scope. action.Should().Throw() - .And.Message.Should().Contain("first failure") - .And.Contain("second failure"); + .WithMessage("Assert.ContainsSingle failed. Expected collection to contain exactly one element but found 3 element(s). 'collection' expression: 'items'."); } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs index c41eba3302..44d73e5432 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs @@ -7,11 +7,11 @@ namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; public partial class AssertTests { - #region ThrowAssertFailed tests + #region ReportSoftAssertFailure tests // See https://github.com/dotnet/sdk/issues/25373 - public void ThrowAssertFailedDoesNotThrowIfMessageContainsInvalidStringFormatComposite() + public void ReportSoftAssertFailureDoesNotThrowIfMessageContainsInvalidStringFormatComposite() { - Action action = () => Assert.ThrowAssertFailed("name", "{"); + Action action = () => Assert.ReportSoftAssertFailure("name", "{"); action.Should().Throw() .WithMessage("*name failed. {*"); } From 61f218e3581b6bd1b94ab0e4d09f13d987002945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 12 Feb 2026 12:23:21 +0100 Subject: [PATCH 06/17] Update logic to ignore compiler diags --- .../Assertions/Assert.AreEqual.cs | 12 ++-- .../Assertions/Assert.AreSame.cs | 6 +- .../Assertions/Assert.Contains.cs | 27 ++++---- .../TestFramework/Assertions/Assert.Count.cs | 6 +- .../Assertions/Assert.EndsWith.cs | 6 +- .../TestFramework/Assertions/Assert.Fail.cs | 7 +- .../Assertions/Assert.IComparable.cs | 14 ++-- .../Assertions/Assert.Inconclusive.cs | 2 +- .../Assert.IsExactInstanceOfType.cs | 15 +++-- .../Assertions/Assert.IsInstanceOfType.cs | 15 +++-- .../TestFramework/Assertions/Assert.IsNull.cs | 11 ++-- .../TestFramework/Assertions/Assert.IsTrue.cs | 14 ++-- .../Assertions/Assert.Matches.cs | 6 +- .../TestFramework/Assertions/Assert.Scope.cs | 2 +- .../Assertions/Assert.StartsWith.cs | 6 +- .../TestFramework/Assertions/Assert.That.cs | 2 +- .../Assertions/Assert.ThrowsException.cs | 10 +-- .../TestFramework/Assertions/Assert.cs | 45 ++----------- .../TestFramework/Assertions/AssertScope.cs | 13 ++-- .../Assertions/CollectionAssert.cs | 40 ++++++------ .../TestFramework/Assertions/StringAssert.cs | 12 ++-- .../Assertions/AssertTests.ScopeTests.cs | 64 +++++++++++++------ .../AssertTests.ThrowsExceptionTests.cs | 6 +- 23 files changed, 172 insertions(+), 169 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 57ec2d5a33..36d787a01f 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -661,7 +661,7 @@ private static void ThrowAssertAreEqualFailed(object? expected, object? actual, userMessage, ReplaceNulls(expected), ReplaceNulls(actual)); - ReportSoftAssertFailure("Assert.AreEqual", finalMessage); + ThrowAssertFailed("Assert.AreEqual", finalMessage); } private static void ThrowAssertAreEqualFailed(T expected, T actual, T delta, string userMessage) @@ -674,7 +674,7 @@ private static void ThrowAssertAreEqualFailed(T expected, T actual, T delta, expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat)); - ReportSoftAssertFailure("Assert.AreEqual", finalMessage); + ThrowAssertFailed("Assert.AreEqual", finalMessage); } private static void ThrowAssertAreEqualFailed(string? expected, string? actual, bool ignoreCase, CultureInfo culture, string userMessage) @@ -697,7 +697,7 @@ private static void ThrowAssertAreEqualFailed(string? expected, string? actual, finalMessage = FormatStringComparisonMessage(expected, actual, userMessage); } - ReportSoftAssertFailure("Assert.AreEqual", finalMessage); + ThrowAssertFailed("Assert.AreEqual", finalMessage); } /// @@ -1225,7 +1225,7 @@ private static void ThrowAssertAreNotEqualFailed(T notExpected, T actual, T d notExpected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat)); - ReportSoftAssertFailure("Assert.AreNotEqual", finalMessage); + ThrowAssertFailed("Assert.AreNotEqual", finalMessage); } /// @@ -1432,7 +1432,7 @@ private static void ThrowAssertAreNotEqualFailed(object? notExpected, object? ac userMessage, ReplaceNulls(notExpected), ReplaceNulls(actual)); - ReportSoftAssertFailure("Assert.AreNotEqual", finalMessage); + ThrowAssertFailed("Assert.AreNotEqual", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs index 468c7eba9b..38291103a3 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -195,7 +195,7 @@ private static void ThrowAssertAreSameFailed(T? expected, T? actual, string u userMessage); } - ReportSoftAssertFailure("Assert.AreSame", finalMessage); + ThrowAssertFailed("Assert.AreSame", finalMessage); } /// @@ -247,5 +247,5 @@ private static bool IsAreNotSameFailing(T? notExpected, T? actual) => object.ReferenceEquals(notExpected, actual); private static void ThrowAssertAreNotSameFailed(string userMessage) - => ReportSoftAssertFailure("Assert.AreNotSame", userMessage); + => ThrowAssertFailed("Assert.AreNotSame", userMessage); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs index cae66021ca..f9b0cd8868 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -173,8 +173,11 @@ public static T ContainsSingle(Func predicate, IEnumerable collec ThrowAssertSingleMatchFailed(actualCount, userMessage); } - // Unreachable code but compiler cannot work it out + // Within an AssertScope, execution continues past the failure — return default(T) as a placeholder. + // Callers should not depend on this value; the assertion failure will be reported when the scope disposes. +#pragma warning disable CS8603 // Possible null reference return. - Soft assertion: postcondition not enforced in scoped mode. return default; +#pragma warning restore CS8603 // Possible null reference return. } /// @@ -478,7 +481,7 @@ public static void Contains(string substring, string value, StringComparison com { string userMessage = BuildUserMessageForSubstringExpressionAndValueExpression(message, substringExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.ContainsFail, value, substring, userMessage); - ReportSoftAssertFailure("Assert.Contains", finalMessage); + ThrowAssertFailed("Assert.Contains", finalMessage); } } @@ -717,7 +720,7 @@ public static void DoesNotContain(string substring, string value, StringComparis { string userMessage = BuildUserMessageForSubstringExpressionAndValueExpression(message, substringExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainFail, value, substring, userMessage); - ReportSoftAssertFailure("Assert.DoesNotContain", finalMessage); + ThrowAssertFailed("Assert.DoesNotContain", finalMessage); } } @@ -758,13 +761,12 @@ public static void IsInRange(T minValue, T maxValue, T value, string? message { string userMessage = BuildUserMessageForMinValueExpressionAndMaxValueExpressionAndValueExpression(message, minValueExpression, maxValueExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsInRangeFail, value, minValue, maxValue, userMessage); - ReportSoftAssertFailure("IsInRange", finalMessage); + ThrowAssertFailed("IsInRange", finalMessage); } } #endregion // IsInRange - [DoesNotReturn] private static void ThrowAssertSingleMatchFailed(int actualCount, string userMessage) { string finalMessage = string.Format( @@ -772,10 +774,9 @@ private static void ThrowAssertSingleMatchFailed(int actualCount, string userMes FrameworkMessages.ContainsSingleMatchFailMsg, userMessage, actualCount); - ReportHardAssertFailure("Assert.ContainsSingle", finalMessage); + ThrowAssertFailed("Assert.ContainsSingle", finalMessage); } - [DoesNotReturn] private static void ThrowAssertContainsSingleFailed(int actualCount, string userMessage) { string finalMessage = string.Format( @@ -783,7 +784,7 @@ private static void ThrowAssertContainsSingleFailed(int actualCount, string user FrameworkMessages.ContainsSingleFailMsg, userMessage, actualCount); - ReportHardAssertFailure("Assert.ContainsSingle", finalMessage); + ThrowAssertFailed("Assert.ContainsSingle", finalMessage); } private static void ThrowAssertContainsItemFailed(string userMessage) @@ -792,7 +793,7 @@ private static void ThrowAssertContainsItemFailed(string userMessage) CultureInfo.CurrentCulture, FrameworkMessages.ContainsItemFailMsg, userMessage); - ReportSoftAssertFailure("Assert.Contains", finalMessage); + ThrowAssertFailed("Assert.Contains", finalMessage); } private static void ThrowAssertContainsPredicateFailed(string userMessage) @@ -801,7 +802,7 @@ private static void ThrowAssertContainsPredicateFailed(string userMessage) CultureInfo.CurrentCulture, FrameworkMessages.ContainsPredicateFailMsg, userMessage); - ReportSoftAssertFailure("Assert.Contains", finalMessage); + ThrowAssertFailed("Assert.Contains", finalMessage); } private static void ThrowAssertDoesNotContainItemFailed(string userMessage) @@ -810,7 +811,7 @@ private static void ThrowAssertDoesNotContainItemFailed(string userMessage) CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainItemFailMsg, userMessage); - ReportSoftAssertFailure("Assert.DoesNotContain", finalMessage); + ThrowAssertFailed("Assert.DoesNotContain", finalMessage); } private static void ThrowAssertDoesNotContainPredicateFailed(string userMessage) @@ -819,6 +820,6 @@ private static void ThrowAssertDoesNotContainPredicateFailed(string userMessage) CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainPredicateFailMsg, userMessage); - ReportSoftAssertFailure("Assert.DoesNotContain", finalMessage); + ThrowAssertFailed("Assert.DoesNotContain", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs index 5940b0f8b7..0b42d2d777 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -341,7 +341,7 @@ private static void ThrowAssertCountFailed(string assertionName, int expectedCou userMessage, expectedCount, actualCount); - ReportSoftAssertFailure($"Assert.{assertionName}", finalMessage); + ThrowAssertFailed($"Assert.{assertionName}", finalMessage); } private static void ThrowAssertIsNotEmptyFailed(string userMessage) @@ -350,6 +350,6 @@ private static void ThrowAssertIsNotEmptyFailed(string userMessage) CultureInfo.CurrentCulture, FrameworkMessages.IsNotEmptyFailMsg, userMessage); - ReportSoftAssertFailure("Assert.IsNotEmpty", finalMessage); + ThrowAssertFailed("Assert.IsNotEmpty", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs b/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs index ca9ca27605..24a5b9e25f 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -78,7 +78,7 @@ public static void EndsWith([NotNull] string? expectedSuffix, [NotNull] string? { string userMessage = BuildUserMessageForExpectedSuffixExpressionAndValueExpression(message, expectedSuffixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.EndsWithFail, value, expectedSuffix, userMessage); - ReportSoftAssertFailure("Assert.EndsWith", finalMessage); + ThrowAssertFailed("Assert.EndsWith", finalMessage); } } @@ -152,7 +152,7 @@ public static void DoesNotEndWith([NotNull] string? notExpectedSuffix, [NotNull] { string userMessage = BuildUserMessageForNotExpectedSuffixExpressionAndValueExpression(message, notExpectedSuffixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotEndWithFail, value, notExpectedSuffix, userMessage); - ReportSoftAssertFailure("Assert.DoesNotEndWith", finalMessage); + ThrowAssertFailed("Assert.DoesNotEndWith", finalMessage); } } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs b/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs index ce60828296..b5ce9502af 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,5 +22,8 @@ public sealed partial class Assert /// [DoesNotReturn] public static void Fail(string message = "") - => ReportHardAssertFailure("Assert.Fail", BuildUserMessage(message)); + { + LaunchDebuggerIfNeeded(); + throw new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, "Assert.Fail", BuildUserMessage(message))); + } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs b/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs index 86b1b217b1..bbba7a955d 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -306,7 +306,7 @@ private static void ThrowAssertIsGreaterThanFailed(T lowerBound, T value, str userMessage, ReplaceNulls(lowerBound), ReplaceNulls(value)); - ReportSoftAssertFailure("Assert.IsGreaterThan", finalMessage); + ThrowAssertFailed("Assert.IsGreaterThan", finalMessage); } private static void ThrowAssertIsGreaterThanOrEqualToFailed(T lowerBound, T value, string userMessage) @@ -317,7 +317,7 @@ private static void ThrowAssertIsGreaterThanOrEqualToFailed(T lowerBound, T v userMessage, ReplaceNulls(lowerBound), ReplaceNulls(value)); - ReportSoftAssertFailure("Assert.IsGreaterThanOrEqualTo", finalMessage); + ThrowAssertFailed("Assert.IsGreaterThanOrEqualTo", finalMessage); } private static void ThrowAssertIsLessThanFailed(T upperBound, T value, string userMessage) @@ -328,7 +328,7 @@ private static void ThrowAssertIsLessThanFailed(T upperBound, T value, string userMessage, ReplaceNulls(upperBound), ReplaceNulls(value)); - ReportSoftAssertFailure("Assert.IsLessThan", finalMessage); + ThrowAssertFailed("Assert.IsLessThan", finalMessage); } private static void ThrowAssertIsLessThanOrEqualToFailed(T upperBound, T value, string userMessage) @@ -339,7 +339,7 @@ private static void ThrowAssertIsLessThanOrEqualToFailed(T upperBound, T valu userMessage, ReplaceNulls(upperBound), ReplaceNulls(value)); - ReportSoftAssertFailure("Assert.IsLessThanOrEqualTo", finalMessage); + ThrowAssertFailed("Assert.IsLessThanOrEqualTo", finalMessage); } private static void ThrowAssertIsPositiveFailed(T value, string userMessage) @@ -349,7 +349,7 @@ private static void ThrowAssertIsPositiveFailed(T value, string userMessage) FrameworkMessages.IsPositiveFailMsg, userMessage, ReplaceNulls(value)); - ReportSoftAssertFailure("Assert.IsPositive", finalMessage); + ThrowAssertFailed("Assert.IsPositive", finalMessage); } private static void ThrowAssertIsNegativeFailed(T value, string userMessage) @@ -359,6 +359,6 @@ private static void ThrowAssertIsNegativeFailed(T value, string userMessage) FrameworkMessages.IsNegativeFailMsg, userMessage, ReplaceNulls(value)); - ReportSoftAssertFailure("Assert.IsNegative", finalMessage); + ThrowAssertFailed("Assert.IsNegative", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs b/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs index cca665dbb8..dd1af1f831 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index b5260eefe5..4eab372acd 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -287,6 +287,7 @@ internal void ComputeAssertion(string valueExpression) /// is not exactly the type /// of . /// +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type? expectedType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsExactInstanceOfTypeFailing(value, expectedType)) @@ -294,12 +295,13 @@ public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type ThrowAssertIsExactInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); } } +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type? expectedType, [InterpolatedStringHandlerArgument(nameof(value), nameof(expectedType))] ref AssertIsExactInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). => message.ComputeAssertion(valueExpression); #pragma warning restore CS8777 // Parameter must have a non-null value when exiting. @@ -308,17 +310,19 @@ public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type /// type and throws an exception if the generic type does not match exactly. /// /// The expected exact type of . +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static T IsExactInstanceOfType([NotNull] object? value, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { IsExactInstanceOfType(value, typeof(T), message, valueExpression); return (T)value; } +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 public static T IsExactInstanceOfType([NotNull] object? value, [InterpolatedStringHandlerArgument(nameof(value))] ref AssertGenericIsExactInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). { message.ComputeAssertion(valueExpression); return (T)value!; @@ -328,7 +332,6 @@ public static T IsExactInstanceOfType([NotNull] object? value, [InterpolatedS private static bool IsExactInstanceOfTypeFailing([NotNullWhen(false)] object? value, [NotNullWhen(false)] Type? expectedType) => expectedType is null || value is null || value.GetType() != expectedType; - [DoesNotReturn] private static void ThrowAssertIsExactInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) { string finalMessage = userMessage; @@ -342,7 +345,7 @@ private static void ThrowAssertIsExactInstanceOfTypeFailed(object? value, Type? value.GetType().ToString()); } - ReportHardAssertFailure("Assert.IsExactInstanceOfType", finalMessage); + ThrowAssertFailed("Assert.IsExactInstanceOfType", finalMessage); } /// @@ -415,6 +418,6 @@ private static void ThrowAssertIsNotExactInstanceOfTypeFailed(object? value, Typ value!.GetType().ToString()); } - ReportSoftAssertFailure("Assert.IsNotExactInstanceOfType", finalMessage); + ThrowAssertFailed("Assert.IsNotExactInstanceOfType", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index cef8c6e26a..bb47726666 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -288,6 +288,7 @@ internal void ComputeAssertion(string valueExpression) /// is not in the inheritance hierarchy /// of . /// +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? expectedType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsInstanceOfTypeFailing(value, expectedType)) @@ -295,12 +296,13 @@ public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? exp ThrowAssertIsInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); } } +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? expectedType, [InterpolatedStringHandlerArgument(nameof(value), nameof(expectedType))] ref AssertIsInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). => message.ComputeAssertion(valueExpression); #pragma warning restore CS8777 // Parameter must have a non-null value when exiting. @@ -310,17 +312,19 @@ public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? exp /// inheritance hierarchy of the object. /// /// The expected type of . +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static T IsInstanceOfType([NotNull] object? value, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { IsInstanceOfType(value, typeof(T), message, valueExpression); return (T)value!; } +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 public static T IsInstanceOfType([NotNull] object? value, [InterpolatedStringHandlerArgument(nameof(value))] ref AssertGenericIsInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). { message.ComputeAssertion(valueExpression); return (T)value!; @@ -330,7 +334,6 @@ public static T IsInstanceOfType([NotNull] object? value, [InterpolatedString private static bool IsInstanceOfTypeFailing([NotNullWhen(false)] object? value, [NotNullWhen(false)] Type? expectedType) => expectedType == null || value == null || !expectedType.IsInstanceOfType(value); - [DoesNotReturn] private static void ThrowAssertIsInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) { string finalMessage = userMessage; @@ -344,7 +347,7 @@ private static void ThrowAssertIsInstanceOfTypeFailed(object? value, Type? expec value.GetType().ToString()); } - ReportHardAssertFailure("Assert.IsInstanceOfType", finalMessage); + ThrowAssertFailed("Assert.IsInstanceOfType", finalMessage); } /// @@ -419,6 +422,6 @@ private static void ThrowAssertIsNotInstanceOfTypeFailed(object? value, Type? wr value!.GetType().ToString()); } - ReportSoftAssertFailure("Assert.IsNotInstanceOfType", finalMessage); + ThrowAssertFailed("Assert.IsNotInstanceOfType", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index a26d6826df..5e93b684aa 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -159,13 +159,13 @@ public static void IsNull(object? value, string? message = "", [CallerArgumentEx private static bool IsNullFailing(object? value) => value is not null; private static void ThrowAssertIsNullFailed(string? message) - => ReportSoftAssertFailure("Assert.IsNull", message); + => ThrowAssertFailed("Assert.IsNull", message); /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 public static void IsNotNull([NotNull] object? value, [InterpolatedStringHandlerArgument(nameof(value))] ref AssertIsNotNullInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). => message.ComputeAssertion(valueExpression); #pragma warning restore CS8777 // Parameter must have a non-null value when exiting. @@ -187,6 +187,7 @@ public static void IsNotNull([NotNull] object? value, [InterpolatedStringHandler /// /// Thrown if is null. /// +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static void IsNotNull([NotNull] object? value, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsNotNullFailing(value)) @@ -194,10 +195,10 @@ public static void IsNotNull([NotNull] object? value, string? message = "", [Cal ThrowAssertIsNotNullFailed(BuildUserMessageForValueExpression(message, valueExpression)); } } +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. private static bool IsNotNullFailing([NotNullWhen(false)] object? value) => value is null; - [DoesNotReturn] private static void ThrowAssertIsNotNullFailed(string? message) - => ReportHardAssertFailure("Assert.IsNotNull", message); + => ThrowAssertFailed("Assert.IsNotNull", message); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs index 344a91d2d2..b091456f1a 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -124,7 +124,7 @@ internal void ComputeAssertion(string conditionExpression) /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 - public static void IsTrue(bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsTrueInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") + public static void IsTrue([DoesNotReturnIf(false)] bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsTrueInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") #pragma warning restore IDE0060 // Remove unused parameter => message.ComputeAssertion(conditionExpression); @@ -146,7 +146,7 @@ public static void IsTrue(bool? condition, [InterpolatedStringHandlerArgument(na /// /// Thrown if is false. /// - public static void IsTrue(bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") + public static void IsTrue([DoesNotReturnIf(false)] bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") { if (IsTrueFailing(condition)) { @@ -158,11 +158,11 @@ private static bool IsTrueFailing(bool? condition) => condition is false or null; private static void ThrowAssertIsTrueFailed(string? message) - => ReportSoftAssertFailure("Assert.IsTrue", message); + => ThrowAssertFailed("Assert.IsTrue", message); /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 - public static void IsFalse(bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsFalseInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") + public static void IsFalse([DoesNotReturnIf(true)] bool? condition, [InterpolatedStringHandlerArgument(nameof(condition))] ref AssertIsFalseInterpolatedStringHandler message, [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") #pragma warning restore IDE0060 // Remove unused parameter => message.ComputeAssertion(conditionExpression); @@ -184,7 +184,7 @@ public static void IsFalse(bool? condition, [InterpolatedStringHandlerArgument(n /// /// Thrown if is true. /// - public static void IsFalse(bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") + public static void IsFalse([DoesNotReturnIf(true)] bool? condition, string? message = "", [CallerArgumentExpression(nameof(condition))] string conditionExpression = "") { if (IsFalseFailing(condition)) { @@ -196,5 +196,5 @@ private static bool IsFalseFailing(bool? condition) => condition is true or null; private static void ThrowAssertIsFalseFailed(string userMessage) - => ReportSoftAssertFailure("Assert.IsFalse", userMessage); + => ThrowAssertFailed("Assert.IsFalse", userMessage); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs b/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs index 0fd3cb54af..d978c94a0f 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -46,7 +46,7 @@ public static void MatchesRegex([NotNull] Regex? pattern, [NotNull] string? valu { string userMessage = BuildUserMessageForPatternExpressionAndValueExpression(message, patternExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsMatchFail, value, pattern, userMessage); - ReportSoftAssertFailure("Assert.MatchesRegex", finalMessage); + ThrowAssertFailed("Assert.MatchesRegex", finalMessage); } } @@ -122,7 +122,7 @@ public static void DoesNotMatchRegex([NotNull] Regex? pattern, [NotNull] string? { string userMessage = BuildUserMessageForPatternExpressionAndValueExpression(message, patternExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsNotMatchFail, value, pattern, userMessage); - ReportSoftAssertFailure("Assert.DoesNotMatchRegex", finalMessage); + ThrowAssertFailed("Assert.DoesNotMatchRegex", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs b/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs index d85b3c2fa8..a46fad8ad3 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Scope.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs b/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs index cd10404326..3fd4059694 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -78,7 +78,7 @@ public static void StartsWith([NotNull] string? expectedPrefix, [NotNull] string { string userMessage = BuildUserMessageForExpectedPrefixExpressionAndValueExpression(message, expectedPrefixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.StartsWithFail, value, expectedPrefix, userMessage); - ReportSoftAssertFailure("Assert.StartsWith", finalMessage); + ThrowAssertFailed("Assert.StartsWith", finalMessage); } } @@ -150,7 +150,7 @@ public static void DoesNotStartWith([NotNull] string? notExpectedPrefix, [NotNul { string userMessage = BuildUserMessageForNotExpectedPrefixExpressionAndValueExpression(message, notExpectedPrefixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotStartWithFail, value, notExpectedPrefix, userMessage); - ReportSoftAssertFailure("Assert.DoesNotStartWith", finalMessage); + ThrowAssertFailed("Assert.DoesNotStartWith", finalMessage); } } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.That.cs b/src/TestFramework/TestFramework/Assertions/Assert.That.cs index e13c803967..6cb03ba5bb 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.That.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.That.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Linq.Expressions; diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs index 1ad0653768..0d1a5a08d7 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -538,7 +538,7 @@ private static async Task IsThrowsAsyncFailingAsync IsThrowsAsyncFailingAsync(Action action, b userMessage, typeof(TException), ex.GetType()); - ReportSoftAssertFailure("Assert." + assertMethodName, finalMessage); + ThrowAssertFailed("Assert." + assertMethodName, finalMessage); }, ex); } @@ -590,7 +590,7 @@ private static ThrowsExceptionState IsThrowsFailing(Action action, b FrameworkMessages.NoExceptionThrown, userMessage, typeof(TException)); - ReportSoftAssertFailure("Assert." + assertMethodName, finalMessage); + ThrowAssertFailed("Assert." + assertMethodName, finalMessage); }, null); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 78def33fa7..71b73638f7 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -26,42 +26,8 @@ private Assert() public static Assert That { get; } = new(); /// - /// Reports a hard assertion failure. This always throws immediately, even within an , - /// and triggers the debugger if configured. Use for assertions whose annotations change the - /// type of a variable (e.g., [NotNull]) where continuing execution would cause downstream errors, - /// and for precondition violations (e.g., a required parameter is ). - /// - /// - /// name of the assertion throwing an exception. - /// - /// - /// The assertion failure message. - /// - [DoesNotReturn] - [StackTraceHidden] - internal static void ReportHardAssertFailure(string assertionName, string? message) - => ReportHardAssertFailure(new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message))); - - /// - /// Reports a hard assertion failure using a pre-built exception. This always throws immediately, - /// even within an , and triggers the debugger if configured. - /// - /// - /// The assertion failure exception to throw. - /// - [DoesNotReturn] - [StackTraceHidden] - internal static void ReportHardAssertFailure(AssertFailedException exception) - { - LaunchDebuggerIfNeeded(); - throw exception; - } - - /// - /// Reports a soft assertion failure. Within an , the failure is collected + /// Reports an assertion failure. Within an , the failure is collected /// and execution continues. Outside a scope, the failure is thrown immediately. - /// Does not trigger the debugger — the debugger is triggered when the scope is disposed - /// and the collected failures are reported. /// /// /// name of the assertion throwing an exception. @@ -70,7 +36,7 @@ internal static void ReportHardAssertFailure(AssertFailedException exception) /// The assertion failure message. /// [StackTraceHidden] - internal static void ReportSoftAssertFailure(string assertionName, string? message) + internal static void ThrowAssertFailed(string assertionName, string? message) { var assertionFailedException = new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); AssertScope? scope = AssertScope.Current; @@ -84,7 +50,7 @@ internal static void ReportSoftAssertFailure(string assertionName, string? messa } [StackTraceHidden] - private static void LaunchDebuggerIfNeeded() + internal static void LaunchDebuggerIfNeeded() { if (ShouldLaunchDebugger()) { @@ -232,7 +198,8 @@ internal static void CheckParameterNotNull([NotNull] object? param, string asser if (param is null) { string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.NullParameterToAssert, parameterName); - ReportHardAssertFailure(assertionName, finalMessage); + LaunchDebuggerIfNeeded(); + throw new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, finalMessage)); } } diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs index 5b467b8b5d..22b6a2862b 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertScope.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -58,15 +58,16 @@ public void Dispose() if (_errors.Count == 1 && _errors.TryDequeue(out AssertFailedException? singleError)) { - Assert.ReportHardAssertFailure(singleError); + Assert.LaunchDebuggerIfNeeded(); + throw singleError; } if (!_errors.IsEmpty) { - Assert.ReportHardAssertFailure( - new AssertFailedException( - string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), - new AggregateException(_errors))); + Assert.LaunchDebuggerIfNeeded(); + throw new AssertFailedException( + string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), + new AggregateException(_errors)); } } } diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs index aef4d1c031..8e5ee9f59c 100644 --- a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -79,7 +79,7 @@ public static void Contains([NotNull] ICollection? collection, object? element, } } - Assert.ReportSoftAssertFailure("CollectionAssert.Contains", Assert.BuildUserMessage(message)); + Assert.ThrowAssertFailed("CollectionAssert.Contains", Assert.BuildUserMessage(message)); } /// @@ -126,7 +126,7 @@ public static void DoesNotContain([NotNull] ICollection? collection, object? ele { if (object.Equals(current, element)) { - Assert.ReportSoftAssertFailure("CollectionAssert.DoesNotContain", Assert.BuildUserMessage(message)); + Assert.ThrowAssertFailed("CollectionAssert.DoesNotContain", Assert.BuildUserMessage(message)); } } } @@ -165,7 +165,7 @@ public static void AllItemsAreNotNull([NotNull] ICollection? collection, string? { if (current == null) { - Assert.ReportSoftAssertFailure("CollectionAssert.AllItemsAreNotNull", Assert.BuildUserMessage(message)); + Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreNotNull", Assert.BuildUserMessage(message)); } } } @@ -226,7 +226,7 @@ public static void AllItemsAreUnique([NotNull] ICollection? collection, string? userMessage, FrameworkMessages.Common_NullInMessages); - Assert.ReportSoftAssertFailure("CollectionAssert.AllItemsAreUnique", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); } } else @@ -240,7 +240,7 @@ public static void AllItemsAreUnique([NotNull] ICollection? collection, string? userMessage, Assert.ReplaceNulls(current)); - Assert.ReportSoftAssertFailure("CollectionAssert.AllItemsAreUnique", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); } } } @@ -303,11 +303,11 @@ public static void IsSubsetOf([NotNull] ICollection? subset, [NotNull] ICollecti string userMessage = Assert.BuildUserMessage(message); if (string.IsNullOrEmpty(userMessage)) { - Assert.ReportSoftAssertFailure("CollectionAssert.IsSubsetOf", returnedSubsetValueMessage); + Assert.ThrowAssertFailed("CollectionAssert.IsSubsetOf", returnedSubsetValueMessage); } else { - Assert.ReportSoftAssertFailure("CollectionAssert.IsSubsetOf", $"{returnedSubsetValueMessage} {userMessage}"); + Assert.ThrowAssertFailed("CollectionAssert.IsSubsetOf", $"{returnedSubsetValueMessage} {userMessage}"); } } } @@ -357,7 +357,7 @@ public static void IsNotSubsetOf([NotNull] ICollection? subset, [NotNull] IColle Tuple> isSubsetValue = IsSubsetOfHelper(subset, superset); if (isSubsetValue.Item1) { - Assert.ReportSoftAssertFailure("CollectionAssert.IsNotSubsetOf", Assert.BuildUserMessage(message)); + Assert.ThrowAssertFailed("CollectionAssert.IsNotSubsetOf", Assert.BuildUserMessage(message)); } } @@ -476,7 +476,7 @@ public static void AreEquivalent( // Check whether one is null while the other is not. if (expected == null != (actual == null)) { - Assert.ReportSoftAssertFailure("CollectionAssert.AreEquivalent", Assert.BuildUserMessage(message)); + Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", Assert.BuildUserMessage(message)); } // If the references are the same or both collections are null, they are equivalent. @@ -500,7 +500,7 @@ public static void AreEquivalent( userMessage, expectedCollectionCount, actualCollectionCount); - Assert.ReportSoftAssertFailure("CollectionAssert.AreEquivalent", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", finalMessage); } // If both collections are empty, they are equivalent. @@ -520,7 +520,7 @@ public static void AreEquivalent( expectedCount.ToString(CultureInfo.CurrentCulture.NumberFormat), Assert.ReplaceNulls(mismatchedElement), actualCount.ToString(CultureInfo.CurrentCulture.NumberFormat)); - Assert.ReportSoftAssertFailure("CollectionAssert.AreEquivalent", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", finalMessage); } // All the elements and counts matched. @@ -654,7 +654,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothCollectionsSameReference, userMessage); - Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); } DebugEx.Assert(actual is not null, "actual is not null here"); @@ -674,7 +674,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothCollectionsEmpty, userMessage); - Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); } // Search for a mismatched element. @@ -685,7 +685,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothSameElements, userMessage); - Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); } } @@ -755,7 +755,7 @@ public static void AllItemsAreInstancesOfType( i, expectedType.ToString(), element.GetType().ToString()); - Assert.ReportSoftAssertFailure("CollectionAssert.AllItemsAreInstancesOfType", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreInstancesOfType", finalMessage); } i++; @@ -816,7 +816,7 @@ public static void AreEqual(ICollection? expected, ICollection? actual, string? if (!AreCollectionsEqual(expected, actual, new ObjectComparer(), ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ReportSoftAssertFailure("CollectionAssert.AreEqual", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreEqual", finalMessage); } } @@ -870,7 +870,7 @@ public static void AreNotEqual(ICollection? notExpected, ICollection? actual, st if (AreCollectionsEqual(notExpected, actual, new ObjectComparer(), ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEqual", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreNotEqual", finalMessage); } } @@ -928,7 +928,7 @@ public static void AreEqual(ICollection? expected, ICollection? actual, [NotNull if (!AreCollectionsEqual(expected, actual, comparer, ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ReportSoftAssertFailure("CollectionAssert.AreEqual", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreEqual", finalMessage); } } @@ -986,7 +986,7 @@ public static void AreNotEqual(ICollection? notExpected, ICollection? actual, [N if (AreCollectionsEqual(notExpected, actual, comparer, ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ReportSoftAssertFailure("CollectionAssert.AreNotEqual", finalMessage); + Assert.ThrowAssertFailed("CollectionAssert.AreNotEqual", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/StringAssert.cs b/src/TestFramework/TestFramework/Assertions/StringAssert.cs index 69037de0dd..2e53aff985 100644 --- a/src/TestFramework/TestFramework/Assertions/StringAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/StringAssert.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -122,7 +122,7 @@ public static void Contains([NotNull] string? value, [NotNull] string? substring { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.ContainsFail, value, substring, userMessage); - Assert.ReportSoftAssertFailure("StringAssert.Contains", finalMessage); + Assert.ThrowAssertFailed("StringAssert.Contains", finalMessage); } } @@ -219,7 +219,7 @@ public static void StartsWith([NotNull] string? value, [NotNull] string? substri { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.StartsWithFail, value, substring, userMessage); - Assert.ReportSoftAssertFailure("StringAssert.StartsWith", finalMessage); + Assert.ThrowAssertFailed("StringAssert.StartsWith", finalMessage); } } @@ -316,7 +316,7 @@ public static void EndsWith([NotNull] string? value, [NotNull] string? substring { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.EndsWithFail, value, substring, userMessage); - Assert.ReportSoftAssertFailure("StringAssert.EndsWith", finalMessage); + Assert.ThrowAssertFailed("StringAssert.EndsWith", finalMessage); } } @@ -371,7 +371,7 @@ public static void Matches([NotNull] string? value, [NotNull] Regex? pattern, st { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsMatchFail, value, pattern, userMessage); - Assert.ReportSoftAssertFailure("StringAssert.Matches", finalMessage); + Assert.ThrowAssertFailed("StringAssert.Matches", finalMessage); } } @@ -422,7 +422,7 @@ public static void DoesNotMatch([NotNull] string? value, [NotNull] Regex? patter { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsNotMatchFail, value, pattern, userMessage); - Assert.ReportSoftAssertFailure("StringAssert.DoesNotMatch", finalMessage); + Assert.ThrowAssertFailed("StringAssert.DoesNotMatch", finalMessage); } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index c166ddfb05..f99c7caea8 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -122,7 +122,7 @@ public void Scope_AssertFail_IsHardFailure() .WithMessage("Assert.Fail failed. first failure"); } - public void Scope_AssertIsNotNull_IsHardFailure() + public void Scope_AssertIsNotNull_IsSoftFailure() { object? value = null; Action action = () => @@ -130,16 +130,22 @@ public void Scope_AssertIsNotNull_IsHardFailure() using (Assert.Scope()) { Assert.IsNotNull(value); - Assert.IsTrue(true); // should not be reached + Assert.AreEqual(1, 2); } }; - // Assert.IsNotNull is a hard assertion — it throws immediately, even within a scope. - action.Should().Throw() - .WithMessage("Assert.IsNotNull failed. 'value' expression: 'value'."); + // Assert.IsNotNull is a soft assertion — failure is collected within a scope. + AggregateException innerException = action.Should().Throw() + .WithMessage("2 assertion(s) failed within the assert scope.") + .WithInnerException() + .Which; + + innerException.InnerExceptions.Should().HaveCount(2); + innerException.InnerExceptions[0].Message.Should().Be("Assert.IsNotNull failed. 'value' expression: 'value'."); + innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } - public void Scope_AssertIsInstanceOfType_IsHardFailure() + public void Scope_AssertIsInstanceOfType_IsSoftFailure() { object value = "hello"; Action action = () => @@ -147,16 +153,22 @@ public void Scope_AssertIsInstanceOfType_IsHardFailure() using (Assert.Scope()) { Assert.IsInstanceOfType(value, typeof(int)); - Assert.IsTrue(true); // should not be reached + Assert.AreEqual(1, 2); } }; - // Assert.IsInstanceOfType is a hard assertion — it throws immediately, even within a scope. - action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: 'value'. Expected type:. Actual type:."); + // Assert.IsInstanceOfType is a soft assertion — failure is collected within a scope. + AggregateException innerException = action.Should().Throw() + .WithMessage("2 assertion(s) failed within the assert scope.") + .WithInnerException() + .Which; + + innerException.InnerExceptions.Should().HaveCount(2); + innerException.InnerExceptions[0].Message.Should().Be("Assert.IsInstanceOfType failed. 'value' expression: 'value'. Expected type:. Actual type:."); + innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } - public void Scope_AssertIsExactInstanceOfType_IsHardFailure() + public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() { object value = "hello"; Action action = () => @@ -164,16 +176,22 @@ public void Scope_AssertIsExactInstanceOfType_IsHardFailure() using (Assert.Scope()) { Assert.IsExactInstanceOfType(value, typeof(object)); - Assert.IsTrue(true); // should not be reached + Assert.AreEqual(1, 2); } }; - // Assert.IsExactInstanceOfType is a hard assertion — it throws immediately, even within a scope. - action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: 'value'. Expected exact type:. Actual type:."); + // Assert.IsExactInstanceOfType is a soft assertion — failure is collected within a scope. + AggregateException innerException = action.Should().Throw() + .WithMessage("2 assertion(s) failed within the assert scope.") + .WithInnerException() + .Which; + + innerException.InnerExceptions.Should().HaveCount(2); + innerException.InnerExceptions[0].Message.Should().Be("Assert.IsExactInstanceOfType failed. 'value' expression: 'value'. Expected exact type:. Actual type:."); + innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } - public void Scope_AssertContainsSingle_IsHardFailure() + public void Scope_AssertContainsSingle_IsSoftFailure() { int[] items = [1, 2, 3]; Action action = () => @@ -181,12 +199,18 @@ public void Scope_AssertContainsSingle_IsHardFailure() using (Assert.Scope()) { Assert.ContainsSingle(items); - Assert.IsTrue(true); // should not be reached + Assert.AreEqual(1, 2); } }; - // Assert.ContainsSingle is a hard assertion — it throws immediately, even within a scope. - action.Should().Throw() - .WithMessage("Assert.ContainsSingle failed. Expected collection to contain exactly one element but found 3 element(s). 'collection' expression: 'items'."); + // Assert.ContainsSingle is a soft assertion — failure is collected within a scope. + AggregateException innerException = action.Should().Throw() + .WithMessage("2 assertion(s) failed within the assert scope.") + .WithInnerException() + .Which; + + innerException.InnerExceptions.Should().HaveCount(2); + innerException.InnerExceptions[0].Message.Should().Be("Assert.ContainsSingle failed. Expected collection to contain exactly one element but found 3 element(s). 'collection' expression: 'items'."); + innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs index 44d73e5432..c41eba3302 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs @@ -7,11 +7,11 @@ namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; public partial class AssertTests { - #region ReportSoftAssertFailure tests + #region ThrowAssertFailed tests // See https://github.com/dotnet/sdk/issues/25373 - public void ReportSoftAssertFailureDoesNotThrowIfMessageContainsInvalidStringFormatComposite() + public void ThrowAssertFailedDoesNotThrowIfMessageContainsInvalidStringFormatComposite() { - Action action = () => Assert.ReportSoftAssertFailure("name", "{"); + Action action = () => Assert.ThrowAssertFailed("name", "{"); action.Should().Throw() .WithMessage("*name failed. {*"); } From 888a8a64dba7157b494fb1e0b0d72fe647e983cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 12 Feb 2026 12:44:51 +0100 Subject: [PATCH 07/17] More refactoring --- .../Assertions/Assert.AreEqual.cs | 15 +++++--- .../Assertions/Assert.AreSame.cs | 6 ++- .../Assertions/Assert.Contains.cs | 24 +++++++----- .../TestFramework/Assertions/Assert.Count.cs | 6 ++- .../Assertions/Assert.EndsWith.cs | 4 +- .../TestFramework/Assertions/Assert.Fail.cs | 7 +--- .../Assertions/Assert.IComparable.cs | 18 ++++++--- .../Assert.IsExactInstanceOfType.cs | 6 ++- .../Assertions/Assert.IsInstanceOfType.cs | 6 ++- .../TestFramework/Assertions/Assert.IsNull.cs | 5 ++- .../TestFramework/Assertions/Assert.IsTrue.cs | 5 ++- .../Assertions/Assert.Matches.cs | 4 +- .../Assertions/Assert.StartsWith.cs | 4 +- .../Assertions/Assert.ThrowsException.cs | 8 ++-- .../TestFramework/Assertions/Assert.cs | 31 +++++++++++---- .../TestFramework/Assertions/AssertScope.cs | 10 ++--- .../Assertions/CollectionAssert.cs | 38 +++++++++---------- .../TestFramework/Assertions/StringAssert.cs | 10 ++--- .../AssertTests.ThrowsExceptionTests.cs | 6 +-- 19 files changed, 126 insertions(+), 87 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 36d787a01f..65146931b3 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -642,6 +642,7 @@ private static string FormatStringDifferenceMessage(string expected, string actu new string('-', adjustedCaretPosition) + "^"); } + [DoesNotReturn] private static void ThrowAssertAreEqualFailed(object? expected, object? actual, string userMessage) { string finalMessage = actual != null && expected != null && !actual.GetType().Equals(expected.GetType()) @@ -661,9 +662,10 @@ private static void ThrowAssertAreEqualFailed(object? expected, object? actual, userMessage, ReplaceNulls(expected), ReplaceNulls(actual)); - ThrowAssertFailed("Assert.AreEqual", finalMessage); + ReportAssertFailed("Assert.AreEqual", finalMessage); } + [DoesNotReturn] private static void ThrowAssertAreEqualFailed(T expected, T actual, T delta, string userMessage) where T : struct, IConvertible { @@ -674,9 +676,10 @@ private static void ThrowAssertAreEqualFailed(T expected, T actual, T delta, expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat)); - ThrowAssertFailed("Assert.AreEqual", finalMessage); + ReportAssertFailed("Assert.AreEqual", finalMessage); } + [DoesNotReturn] private static void ThrowAssertAreEqualFailed(string? expected, string? actual, bool ignoreCase, CultureInfo culture, string userMessage) { string finalMessage; @@ -697,7 +700,7 @@ private static void ThrowAssertAreEqualFailed(string? expected, string? actual, finalMessage = FormatStringComparisonMessage(expected, actual, userMessage); } - ThrowAssertFailed("Assert.AreEqual", finalMessage); + ReportAssertFailed("Assert.AreEqual", finalMessage); } /// @@ -1215,6 +1218,7 @@ private static bool AreNotEqualFailing(double notExpected, double actual, double return Math.Abs(notExpected - actual) <= delta; } + [DoesNotReturn] private static void ThrowAssertAreNotEqualFailed(T notExpected, T actual, T delta, string userMessage) where T : struct, IConvertible { @@ -1225,7 +1229,7 @@ private static void ThrowAssertAreNotEqualFailed(T notExpected, T actual, T d notExpected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat)); - ThrowAssertFailed("Assert.AreNotEqual", finalMessage); + ReportAssertFailed("Assert.AreNotEqual", finalMessage); } /// @@ -1424,6 +1428,7 @@ private static bool AreNotEqualFailing(string? notExpected, string? actual, bool private static bool AreNotEqualFailing(T? notExpected, T? actual, IEqualityComparer? comparer) => (comparer ?? EqualityComparer.Default).Equals(notExpected!, actual!); + [DoesNotReturn] private static void ThrowAssertAreNotEqualFailed(object? notExpected, object? actual, string userMessage) { string finalMessage = string.Format( @@ -1432,7 +1437,7 @@ private static void ThrowAssertAreNotEqualFailed(object? notExpected, object? ac userMessage, ReplaceNulls(notExpected), ReplaceNulls(actual)); - ThrowAssertFailed("Assert.AreNotEqual", finalMessage); + ReportAssertFailed("Assert.AreNotEqual", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs index 38291103a3..723662c952 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs @@ -184,6 +184,7 @@ public static void AreSame(T? expected, T? actual, string? message = "", [Cal private static bool IsAreSameFailing(T? expected, T? actual) => !object.ReferenceEquals(expected, actual); + [DoesNotReturn] private static void ThrowAssertAreSameFailed(T? expected, T? actual, string userMessage) { string finalMessage = userMessage; @@ -195,7 +196,7 @@ private static void ThrowAssertAreSameFailed(T? expected, T? actual, string u userMessage); } - ThrowAssertFailed("Assert.AreSame", finalMessage); + ReportAssertFailed("Assert.AreSame", finalMessage); } /// @@ -246,6 +247,7 @@ public static void AreNotSame(T? notExpected, T? actual, string? message = "" private static bool IsAreNotSameFailing(T? notExpected, T? actual) => object.ReferenceEquals(notExpected, actual); + [DoesNotReturn] private static void ThrowAssertAreNotSameFailed(string userMessage) - => ThrowAssertFailed("Assert.AreNotSame", userMessage); + => ReportAssertFailed("Assert.AreNotSame", userMessage); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs index f9b0cd8868..3a1c4cfe1e 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs @@ -481,7 +481,7 @@ public static void Contains(string substring, string value, StringComparison com { string userMessage = BuildUserMessageForSubstringExpressionAndValueExpression(message, substringExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.ContainsFail, value, substring, userMessage); - ThrowAssertFailed("Assert.Contains", finalMessage); + ReportAssertFailed("Assert.Contains", finalMessage); } } @@ -720,7 +720,7 @@ public static void DoesNotContain(string substring, string value, StringComparis { string userMessage = BuildUserMessageForSubstringExpressionAndValueExpression(message, substringExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainFail, value, substring, userMessage); - ThrowAssertFailed("Assert.DoesNotContain", finalMessage); + ReportAssertFailed("Assert.DoesNotContain", finalMessage); } } @@ -761,12 +761,13 @@ public static void IsInRange(T minValue, T maxValue, T value, string? message { string userMessage = BuildUserMessageForMinValueExpressionAndMaxValueExpressionAndValueExpression(message, minValueExpression, maxValueExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsInRangeFail, value, minValue, maxValue, userMessage); - ThrowAssertFailed("IsInRange", finalMessage); + ReportAssertFailed("IsInRange", finalMessage); } } #endregion // IsInRange + [DoesNotReturn] private static void ThrowAssertSingleMatchFailed(int actualCount, string userMessage) { string finalMessage = string.Format( @@ -774,9 +775,10 @@ private static void ThrowAssertSingleMatchFailed(int actualCount, string userMes FrameworkMessages.ContainsSingleMatchFailMsg, userMessage, actualCount); - ThrowAssertFailed("Assert.ContainsSingle", finalMessage); + ReportAssertFailed("Assert.ContainsSingle", finalMessage); } + [DoesNotReturn] private static void ThrowAssertContainsSingleFailed(int actualCount, string userMessage) { string finalMessage = string.Format( @@ -784,42 +786,46 @@ private static void ThrowAssertContainsSingleFailed(int actualCount, string user FrameworkMessages.ContainsSingleFailMsg, userMessage, actualCount); - ThrowAssertFailed("Assert.ContainsSingle", finalMessage); + ReportAssertFailed("Assert.ContainsSingle", finalMessage); } + [DoesNotReturn] private static void ThrowAssertContainsItemFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.ContainsItemFailMsg, userMessage); - ThrowAssertFailed("Assert.Contains", finalMessage); + ReportAssertFailed("Assert.Contains", finalMessage); } + [DoesNotReturn] private static void ThrowAssertContainsPredicateFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.ContainsPredicateFailMsg, userMessage); - ThrowAssertFailed("Assert.Contains", finalMessage); + ReportAssertFailed("Assert.Contains", finalMessage); } + [DoesNotReturn] private static void ThrowAssertDoesNotContainItemFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainItemFailMsg, userMessage); - ThrowAssertFailed("Assert.DoesNotContain", finalMessage); + ReportAssertFailed("Assert.DoesNotContain", finalMessage); } + [DoesNotReturn] private static void ThrowAssertDoesNotContainPredicateFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.DoesNotContainPredicateFailMsg, userMessage); - ThrowAssertFailed("Assert.DoesNotContain", finalMessage); + ReportAssertFailed("Assert.DoesNotContain", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs index 0b42d2d777..c8f64a9dc1 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs @@ -333,6 +333,7 @@ private static void HasCount(string assertionName, int expected, IEnumerable< private static void HasCount(string assertionName, int expected, IEnumerable collection, string? message, string collectionExpression) => HasCount(assertionName, expected, collection.Cast(), message, collectionExpression); + [DoesNotReturn] private static void ThrowAssertCountFailed(string assertionName, int expectedCount, int actualCount, string userMessage) { string finalMessage = string.Format( @@ -341,15 +342,16 @@ private static void ThrowAssertCountFailed(string assertionName, int expectedCou userMessage, expectedCount, actualCount); - ThrowAssertFailed($"Assert.{assertionName}", finalMessage); + ReportAssertFailed($"Assert.{assertionName}", finalMessage); } + [DoesNotReturn] private static void ThrowAssertIsNotEmptyFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, FrameworkMessages.IsNotEmptyFailMsg, userMessage); - ThrowAssertFailed("Assert.IsNotEmpty", finalMessage); + ReportAssertFailed("Assert.IsNotEmpty", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs b/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs index 24a5b9e25f..9ebe0a817c 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.EndsWith.cs @@ -78,7 +78,7 @@ public static void EndsWith([NotNull] string? expectedSuffix, [NotNull] string? { string userMessage = BuildUserMessageForExpectedSuffixExpressionAndValueExpression(message, expectedSuffixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.EndsWithFail, value, expectedSuffix, userMessage); - ThrowAssertFailed("Assert.EndsWith", finalMessage); + ReportAssertFailed("Assert.EndsWith", finalMessage); } } @@ -152,7 +152,7 @@ public static void DoesNotEndWith([NotNull] string? notExpectedSuffix, [NotNull] { string userMessage = BuildUserMessageForNotExpectedSuffixExpressionAndValueExpression(message, notExpectedSuffixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotEndWithFail, value, notExpectedSuffix, userMessage); - ThrowAssertFailed("Assert.DoesNotEndWith", finalMessage); + ReportAssertFailed("Assert.DoesNotEndWith", finalMessage); } } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs b/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs index b5ce9502af..041c10ba45 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,8 +22,5 @@ public sealed partial class Assert /// [DoesNotReturn] public static void Fail(string message = "") - { - LaunchDebuggerIfNeeded(); - throw new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, "Assert.Fail", BuildUserMessage(message))); - } + => ReportAssertFailed("Assert.Fail", BuildUserMessage(message), forceThrow: true); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs b/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs index bbba7a955d..cfc8240ad6 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs @@ -298,6 +298,7 @@ public static void IsNegative(T value, string? message = "", [CallerArgumentE #endregion // IsNegative + [DoesNotReturn] private static void ThrowAssertIsGreaterThanFailed(T lowerBound, T value, string userMessage) { string finalMessage = string.Format( @@ -306,9 +307,10 @@ private static void ThrowAssertIsGreaterThanFailed(T lowerBound, T value, str userMessage, ReplaceNulls(lowerBound), ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsGreaterThan", finalMessage); + ReportAssertFailed("Assert.IsGreaterThan", finalMessage); } + [DoesNotReturn] private static void ThrowAssertIsGreaterThanOrEqualToFailed(T lowerBound, T value, string userMessage) { string finalMessage = string.Format( @@ -317,9 +319,10 @@ private static void ThrowAssertIsGreaterThanOrEqualToFailed(T lowerBound, T v userMessage, ReplaceNulls(lowerBound), ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsGreaterThanOrEqualTo", finalMessage); + ReportAssertFailed("Assert.IsGreaterThanOrEqualTo", finalMessage); } + [DoesNotReturn] private static void ThrowAssertIsLessThanFailed(T upperBound, T value, string userMessage) { string finalMessage = string.Format( @@ -328,9 +331,10 @@ private static void ThrowAssertIsLessThanFailed(T upperBound, T value, string userMessage, ReplaceNulls(upperBound), ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsLessThan", finalMessage); + ReportAssertFailed("Assert.IsLessThan", finalMessage); } + [DoesNotReturn] private static void ThrowAssertIsLessThanOrEqualToFailed(T upperBound, T value, string userMessage) { string finalMessage = string.Format( @@ -339,9 +343,10 @@ private static void ThrowAssertIsLessThanOrEqualToFailed(T upperBound, T valu userMessage, ReplaceNulls(upperBound), ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsLessThanOrEqualTo", finalMessage); + ReportAssertFailed("Assert.IsLessThanOrEqualTo", finalMessage); } + [DoesNotReturn] private static void ThrowAssertIsPositiveFailed(T value, string userMessage) { string finalMessage = string.Format( @@ -349,9 +354,10 @@ private static void ThrowAssertIsPositiveFailed(T value, string userMessage) FrameworkMessages.IsPositiveFailMsg, userMessage, ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsPositive", finalMessage); + ReportAssertFailed("Assert.IsPositive", finalMessage); } + [DoesNotReturn] private static void ThrowAssertIsNegativeFailed(T value, string userMessage) { string finalMessage = string.Format( @@ -359,6 +365,6 @@ private static void ThrowAssertIsNegativeFailed(T value, string userMessage) FrameworkMessages.IsNegativeFailMsg, userMessage, ReplaceNulls(value)); - ThrowAssertFailed("Assert.IsNegative", finalMessage); + ReportAssertFailed("Assert.IsNegative", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index 4eab372acd..b7ed762f06 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -332,6 +332,7 @@ public static T IsExactInstanceOfType([NotNull] object? value, [InterpolatedS private static bool IsExactInstanceOfTypeFailing([NotNullWhen(false)] object? value, [NotNullWhen(false)] Type? expectedType) => expectedType is null || value is null || value.GetType() != expectedType; + [DoesNotReturn] private static void ThrowAssertIsExactInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) { string finalMessage = userMessage; @@ -345,7 +346,7 @@ private static void ThrowAssertIsExactInstanceOfTypeFailed(object? value, Type? value.GetType().ToString()); } - ThrowAssertFailed("Assert.IsExactInstanceOfType", finalMessage); + ReportAssertFailed("Assert.IsExactInstanceOfType", finalMessage); } /// @@ -405,6 +406,7 @@ private static bool IsNotExactInstanceOfTypeFailing(object? value, [NotNullWhen( // Null is not an instance of any type. (value is not null && value.GetType() == wrongType); + [DoesNotReturn] private static void ThrowAssertIsNotExactInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) { string finalMessage = userMessage; @@ -418,6 +420,6 @@ private static void ThrowAssertIsNotExactInstanceOfTypeFailed(object? value, Typ value!.GetType().ToString()); } - ThrowAssertFailed("Assert.IsNotExactInstanceOfType", finalMessage); + ReportAssertFailed("Assert.IsNotExactInstanceOfType", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index bb47726666..d24f2b456a 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -334,6 +334,7 @@ public static T IsInstanceOfType([NotNull] object? value, [InterpolatedString private static bool IsInstanceOfTypeFailing([NotNullWhen(false)] object? value, [NotNullWhen(false)] Type? expectedType) => expectedType == null || value == null || !expectedType.IsInstanceOfType(value); + [DoesNotReturn] private static void ThrowAssertIsInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) { string finalMessage = userMessage; @@ -347,7 +348,7 @@ private static void ThrowAssertIsInstanceOfTypeFailed(object? value, Type? expec value.GetType().ToString()); } - ThrowAssertFailed("Assert.IsInstanceOfType", finalMessage); + ReportAssertFailed("Assert.IsInstanceOfType", finalMessage); } /// @@ -409,6 +410,7 @@ private static bool IsNotInstanceOfTypeFailing(object? value, [NotNullWhen(false // Null is not an instance of any type. (value is not null && wrongType.IsInstanceOfType(value)); + [DoesNotReturn] private static void ThrowAssertIsNotInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) { string finalMessage = userMessage; @@ -422,6 +424,6 @@ private static void ThrowAssertIsNotInstanceOfTypeFailed(object? value, Type? wr value!.GetType().ToString()); } - ThrowAssertFailed("Assert.IsNotInstanceOfType", finalMessage); + ReportAssertFailed("Assert.IsNotInstanceOfType", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index 5e93b684aa..152e4b125a 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -159,7 +159,7 @@ public static void IsNull(object? value, string? message = "", [CallerArgumentEx private static bool IsNullFailing(object? value) => value is not null; private static void ThrowAssertIsNullFailed(string? message) - => ThrowAssertFailed("Assert.IsNull", message); + => ReportAssertFailed("Assert.IsNull", message); /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 @@ -199,6 +199,7 @@ public static void IsNotNull([NotNull] object? value, string? message = "", [Cal private static bool IsNotNullFailing([NotNullWhen(false)] object? value) => value is null; + [DoesNotReturn] private static void ThrowAssertIsNotNullFailed(string? message) - => ThrowAssertFailed("Assert.IsNotNull", message); + => ReportAssertFailed("Assert.IsNotNull", message); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs index b091456f1a..38e22cc71f 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs @@ -158,7 +158,7 @@ private static bool IsTrueFailing(bool? condition) => condition is false or null; private static void ThrowAssertIsTrueFailed(string? message) - => ThrowAssertFailed("Assert.IsTrue", message); + => ReportAssertFailed("Assert.IsTrue", message); /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 @@ -195,6 +195,7 @@ public static void IsFalse([DoesNotReturnIf(true)] bool? condition, string? mess private static bool IsFalseFailing(bool? condition) => condition is true or null; + [DoesNotReturn] private static void ThrowAssertIsFalseFailed(string userMessage) - => ThrowAssertFailed("Assert.IsFalse", userMessage); + => ReportAssertFailed("Assert.IsFalse", userMessage); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs b/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs index d978c94a0f..23b69859c7 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Matches.cs @@ -46,7 +46,7 @@ public static void MatchesRegex([NotNull] Regex? pattern, [NotNull] string? valu { string userMessage = BuildUserMessageForPatternExpressionAndValueExpression(message, patternExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsMatchFail, value, pattern, userMessage); - ThrowAssertFailed("Assert.MatchesRegex", finalMessage); + ReportAssertFailed("Assert.MatchesRegex", finalMessage); } } @@ -122,7 +122,7 @@ public static void DoesNotMatchRegex([NotNull] Regex? pattern, [NotNull] string? { string userMessage = BuildUserMessageForPatternExpressionAndValueExpression(message, patternExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsNotMatchFail, value, pattern, userMessage); - ThrowAssertFailed("Assert.DoesNotMatchRegex", finalMessage); + ReportAssertFailed("Assert.DoesNotMatchRegex", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs b/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs index 3fd4059694..caf58c2a10 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.StartsWith.cs @@ -78,7 +78,7 @@ public static void StartsWith([NotNull] string? expectedPrefix, [NotNull] string { string userMessage = BuildUserMessageForExpectedPrefixExpressionAndValueExpression(message, expectedPrefixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.StartsWithFail, value, expectedPrefix, userMessage); - ThrowAssertFailed("Assert.StartsWith", finalMessage); + ReportAssertFailed("Assert.StartsWith", finalMessage); } } @@ -150,7 +150,7 @@ public static void DoesNotStartWith([NotNull] string? notExpectedPrefix, [NotNul { string userMessage = BuildUserMessageForNotExpectedPrefixExpressionAndValueExpression(message, notExpectedPrefixExpression, valueExpression); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DoesNotStartWithFail, value, notExpectedPrefix, userMessage); - ThrowAssertFailed("Assert.DoesNotStartWith", finalMessage); + ReportAssertFailed("Assert.DoesNotStartWith", finalMessage); } } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs index 0d1a5a08d7..5ef86cd9ad 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs @@ -538,7 +538,7 @@ private static async Task IsThrowsAsyncFailingAsync IsThrowsAsyncFailingAsync(Action action, b userMessage, typeof(TException), ex.GetType()); - ThrowAssertFailed("Assert." + assertMethodName, finalMessage); + ReportAssertFailed("Assert." + assertMethodName, finalMessage); }, ex); } @@ -590,7 +590,7 @@ private static ThrowsExceptionState IsThrowsFailing(Action action, b FrameworkMessages.NoExceptionThrown, userMessage, typeof(TException)); - ThrowAssertFailed("Assert." + assertMethodName, finalMessage); + ReportAssertFailed("Assert." + assertMethodName, finalMessage); }, null); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 71b73638f7..ee4e56c0ea 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -35,18 +35,35 @@ private Assert() /// /// The assertion failure message. /// + /// + /// When , the exception is always thrown, even within an . + /// +#pragma warning disable CS8763 // A method marked [DoesNotReturn] should not return - Deliberately keeping [DoesNotReturn] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). + [DoesNotReturn] [StackTraceHidden] - internal static void ThrowAssertFailed(string assertionName, string? message) + internal static void ReportAssertFailed(string assertionName, string? message, bool forceThrow = false) { var assertionFailedException = new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); - AssertScope? scope = AssertScope.Current; - if (scope is not null) + if (!forceThrow) { - scope.AddError(assertionFailedException); - return; + AssertScope? scope = AssertScope.Current; + if (scope is not null) + { + scope.AddError(assertionFailedException); + return; + } } - throw assertionFailedException; + ThrowAssertFailed(assertionFailedException); + } +#pragma warning restore CS8763 // A method marked [DoesNotReturn] should not return + + [DoesNotReturn] + [StackTraceHidden] + internal static void ThrowAssertFailed(AssertFailedException exception) + { + LaunchDebuggerIfNeeded(); + throw exception; } [StackTraceHidden] diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs index 22b6a2862b..b36bc7962f 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertScope.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -58,16 +58,14 @@ public void Dispose() if (_errors.Count == 1 && _errors.TryDequeue(out AssertFailedException? singleError)) { - Assert.LaunchDebuggerIfNeeded(); - throw singleError; + Assert.ThrowAssertFailed(singleError); } if (!_errors.IsEmpty) { - Assert.LaunchDebuggerIfNeeded(); - throw new AssertFailedException( + Assert.ThrowAssertFailed(new AssertFailedException( string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), - new AggregateException(_errors)); + new AggregateException(_errors))); } } } diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs index 8e5ee9f59c..11b92190ca 100644 --- a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs @@ -79,7 +79,7 @@ public static void Contains([NotNull] ICollection? collection, object? element, } } - Assert.ThrowAssertFailed("CollectionAssert.Contains", Assert.BuildUserMessage(message)); + Assert.ReportAssertFailed("CollectionAssert.Contains", Assert.BuildUserMessage(message)); } /// @@ -126,7 +126,7 @@ public static void DoesNotContain([NotNull] ICollection? collection, object? ele { if (object.Equals(current, element)) { - Assert.ThrowAssertFailed("CollectionAssert.DoesNotContain", Assert.BuildUserMessage(message)); + Assert.ReportAssertFailed("CollectionAssert.DoesNotContain", Assert.BuildUserMessage(message)); } } } @@ -165,7 +165,7 @@ public static void AllItemsAreNotNull([NotNull] ICollection? collection, string? { if (current == null) { - Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreNotNull", Assert.BuildUserMessage(message)); + Assert.ReportAssertFailed("CollectionAssert.AllItemsAreNotNull", Assert.BuildUserMessage(message)); } } } @@ -226,7 +226,7 @@ public static void AllItemsAreUnique([NotNull] ICollection? collection, string? userMessage, FrameworkMessages.Common_NullInMessages); - Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); } } else @@ -240,7 +240,7 @@ public static void AllItemsAreUnique([NotNull] ICollection? collection, string? userMessage, Assert.ReplaceNulls(current)); - Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AllItemsAreUnique", finalMessage); } } } @@ -303,11 +303,11 @@ public static void IsSubsetOf([NotNull] ICollection? subset, [NotNull] ICollecti string userMessage = Assert.BuildUserMessage(message); if (string.IsNullOrEmpty(userMessage)) { - Assert.ThrowAssertFailed("CollectionAssert.IsSubsetOf", returnedSubsetValueMessage); + Assert.ReportAssertFailed("CollectionAssert.IsSubsetOf", returnedSubsetValueMessage); } else { - Assert.ThrowAssertFailed("CollectionAssert.IsSubsetOf", $"{returnedSubsetValueMessage} {userMessage}"); + Assert.ReportAssertFailed("CollectionAssert.IsSubsetOf", $"{returnedSubsetValueMessage} {userMessage}"); } } } @@ -357,7 +357,7 @@ public static void IsNotSubsetOf([NotNull] ICollection? subset, [NotNull] IColle Tuple> isSubsetValue = IsSubsetOfHelper(subset, superset); if (isSubsetValue.Item1) { - Assert.ThrowAssertFailed("CollectionAssert.IsNotSubsetOf", Assert.BuildUserMessage(message)); + Assert.ReportAssertFailed("CollectionAssert.IsNotSubsetOf", Assert.BuildUserMessage(message)); } } @@ -476,7 +476,7 @@ public static void AreEquivalent( // Check whether one is null while the other is not. if (expected == null != (actual == null)) { - Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", Assert.BuildUserMessage(message)); + Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", Assert.BuildUserMessage(message)); } // If the references are the same or both collections are null, they are equivalent. @@ -500,7 +500,7 @@ public static void AreEquivalent( userMessage, expectedCollectionCount, actualCollectionCount); - Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", finalMessage); } // If both collections are empty, they are equivalent. @@ -520,7 +520,7 @@ public static void AreEquivalent( expectedCount.ToString(CultureInfo.CurrentCulture.NumberFormat), Assert.ReplaceNulls(mismatchedElement), actualCount.ToString(CultureInfo.CurrentCulture.NumberFormat)); - Assert.ThrowAssertFailed("CollectionAssert.AreEquivalent", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreEquivalent", finalMessage); } // All the elements and counts matched. @@ -654,7 +654,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothCollectionsSameReference, userMessage); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); } DebugEx.Assert(actual is not null, "actual is not null here"); @@ -674,7 +674,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothCollectionsEmpty, userMessage); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); } // Search for a mismatched element. @@ -685,7 +685,7 @@ public static void AreNotEquivalent( CultureInfo.CurrentCulture, FrameworkMessages.BothSameElements, userMessage); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreNotEquivalent", finalMessage); } } @@ -755,7 +755,7 @@ public static void AllItemsAreInstancesOfType( i, expectedType.ToString(), element.GetType().ToString()); - Assert.ThrowAssertFailed("CollectionAssert.AllItemsAreInstancesOfType", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AllItemsAreInstancesOfType", finalMessage); } i++; @@ -816,7 +816,7 @@ public static void AreEqual(ICollection? expected, ICollection? actual, string? if (!AreCollectionsEqual(expected, actual, new ObjectComparer(), ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ThrowAssertFailed("CollectionAssert.AreEqual", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreEqual", finalMessage); } } @@ -870,7 +870,7 @@ public static void AreNotEqual(ICollection? notExpected, ICollection? actual, st if (AreCollectionsEqual(notExpected, actual, new ObjectComparer(), ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEqual", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreNotEqual", finalMessage); } } @@ -928,7 +928,7 @@ public static void AreEqual(ICollection? expected, ICollection? actual, [NotNull if (!AreCollectionsEqual(expected, actual, comparer, ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ThrowAssertFailed("CollectionAssert.AreEqual", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreEqual", finalMessage); } } @@ -986,7 +986,7 @@ public static void AreNotEqual(ICollection? notExpected, ICollection? actual, [N if (AreCollectionsEqual(notExpected, actual, comparer, ref reason)) { string finalMessage = ConstructFinalMessage(reason, message); - Assert.ThrowAssertFailed("CollectionAssert.AreNotEqual", finalMessage); + Assert.ReportAssertFailed("CollectionAssert.AreNotEqual", finalMessage); } } diff --git a/src/TestFramework/TestFramework/Assertions/StringAssert.cs b/src/TestFramework/TestFramework/Assertions/StringAssert.cs index 2e53aff985..3c65a1ed62 100644 --- a/src/TestFramework/TestFramework/Assertions/StringAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/StringAssert.cs @@ -122,7 +122,7 @@ public static void Contains([NotNull] string? value, [NotNull] string? substring { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.ContainsFail, value, substring, userMessage); - Assert.ThrowAssertFailed("StringAssert.Contains", finalMessage); + Assert.ReportAssertFailed("StringAssert.Contains", finalMessage); } } @@ -219,7 +219,7 @@ public static void StartsWith([NotNull] string? value, [NotNull] string? substri { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.StartsWithFail, value, substring, userMessage); - Assert.ThrowAssertFailed("StringAssert.StartsWith", finalMessage); + Assert.ReportAssertFailed("StringAssert.StartsWith", finalMessage); } } @@ -316,7 +316,7 @@ public static void EndsWith([NotNull] string? value, [NotNull] string? substring { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.EndsWithFail, value, substring, userMessage); - Assert.ThrowAssertFailed("StringAssert.EndsWith", finalMessage); + Assert.ReportAssertFailed("StringAssert.EndsWith", finalMessage); } } @@ -371,7 +371,7 @@ public static void Matches([NotNull] string? value, [NotNull] Regex? pattern, st { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsMatchFail, value, pattern, userMessage); - Assert.ThrowAssertFailed("StringAssert.Matches", finalMessage); + Assert.ReportAssertFailed("StringAssert.Matches", finalMessage); } } @@ -422,7 +422,7 @@ public static void DoesNotMatch([NotNull] string? value, [NotNull] Regex? patter { string userMessage = Assert.BuildUserMessage(message); string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.IsNotMatchFail, value, pattern, userMessage); - Assert.ThrowAssertFailed("StringAssert.DoesNotMatch", finalMessage); + Assert.ReportAssertFailed("StringAssert.DoesNotMatch", finalMessage); } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs index c41eba3302..e773d3ad92 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs @@ -7,11 +7,11 @@ namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; public partial class AssertTests { - #region ThrowAssertFailed tests + #region ReportAssertFailed tests // See https://github.com/dotnet/sdk/issues/25373 - public void ThrowAssertFailedDoesNotThrowIfMessageContainsInvalidStringFormatComposite() + public void ReportAssertFailedDoesNotThrowIfMessageContainsInvalidStringFormatComposite() { - Action action = () => Assert.ThrowAssertFailed("name", "{"); + Action action = () => Assert.ReportAssertFailed("name", "{"); action.Should().Throw() .WithMessage("*name failed. {*"); } From c68b24e11c5efc1858b2bea44bd4633d686b76cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 12 Feb 2026 13:01:08 +0100 Subject: [PATCH 08/17] Cleanup --- .../TestFramework/Assertions/Assert.Contains.cs | 7 ++----- .../Assertions/Assert.IsExactInstanceOfType.cs | 6 +----- .../TestFramework/Assertions/Assert.IsInstanceOfType.cs | 6 +----- .../TestFramework/Assertions/Assert.IsNull.cs | 4 +--- src/TestFramework/TestFramework/Assertions/Assert.cs | 1 - 5 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs index 3a1c4cfe1e..008eb40208 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -173,11 +173,8 @@ public static T ContainsSingle(Func predicate, IEnumerable collec ThrowAssertSingleMatchFailed(actualCount, userMessage); } - // Within an AssertScope, execution continues past the failure — return default(T) as a placeholder. - // Callers should not depend on this value; the assertion failure will be reported when the scope disposes. -#pragma warning disable CS8603 // Possible null reference return. - Soft assertion: postcondition not enforced in scoped mode. + // Unreachable code but compiler cannot work it out return default; -#pragma warning restore CS8603 // Possible null reference return. } /// diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index b7ed762f06..103eb2fb89 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -287,7 +287,6 @@ internal void ComputeAssertion(string valueExpression) /// is not exactly the type /// of . /// -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type? expectedType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsExactInstanceOfTypeFailing(value, expectedType)) @@ -295,7 +294,6 @@ public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type ThrowAssertIsExactInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); } } -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 @@ -310,13 +308,11 @@ public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type /// type and throws an exception if the generic type does not match exactly. /// /// The expected exact type of . -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static T IsExactInstanceOfType([NotNull] object? value, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { IsExactInstanceOfType(value, typeof(T), message, valueExpression); return (T)value; } -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index d24f2b456a..9675a9601d 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -288,7 +288,6 @@ internal void ComputeAssertion(string valueExpression) /// is not in the inheritance hierarchy /// of . /// -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? expectedType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsInstanceOfTypeFailing(value, expectedType)) @@ -296,7 +295,6 @@ public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? exp ThrowAssertIsInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); } } -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 @@ -312,13 +310,11 @@ public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? exp /// inheritance hierarchy of the object. /// /// The expected type of . -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static T IsInstanceOfType([NotNull] object? value, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { IsInstanceOfType(value, typeof(T), message, valueExpression); return (T)value!; } -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index 152e4b125a..e8be59e897 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -187,7 +187,6 @@ public static void IsNotNull([NotNull] object? value, [InterpolatedStringHandler /// /// Thrown if is null. /// -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Deliberately keeping [NotNull] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). public static void IsNotNull([NotNull] object? value, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsNotNullFailing(value)) @@ -195,7 +194,6 @@ public static void IsNotNull([NotNull] object? value, string? message = "", [Cal ThrowAssertIsNotNullFailed(BuildUserMessageForValueExpression(message, valueExpression)); } } -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. private static bool IsNotNullFailing([NotNullWhen(false)] object? value) => value is null; diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index ee4e56c0ea..36f3aa6327 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -215,7 +215,6 @@ internal static void CheckParameterNotNull([NotNull] object? param, string asser if (param is null) { string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.NullParameterToAssert, parameterName); - LaunchDebuggerIfNeeded(); throw new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, finalMessage)); } } From ceb625bb06fad18a4115f1561ca086fc84c6d947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 12 Feb 2026 13:01:15 +0100 Subject: [PATCH 09/17] Update RFC --- .../011-Soft-Assertions-Nullability-Design.md | 188 +++++++++--------- 1 file changed, 93 insertions(+), 95 deletions(-) diff --git a/docs/RFCs/011-Soft-Assertions-Nullability-Design.md b/docs/RFCs/011-Soft-Assertions-Nullability-Design.md index 1b7fc8bc31..f9b60a2f1a 100644 --- a/docs/RFCs/011-Soft-Assertions-Nullability-Design.md +++ b/docs/RFCs/011-Soft-Assertions-Nullability-Design.md @@ -1,8 +1,8 @@ # RFC 011 - Soft Assertions and Nullability Annotation Design - [x] Approved in principle -- [ ] Under discussion -- [ ] Implementation +- [x] Under discussion +- [x] Implementation - [ ] Shipped ## Summary @@ -11,8 +11,6 @@ ## Motivation -### The soft-assertion goal - Today, every MSTest assertion throws `AssertFailedException` on failure. With `Assert.Scope()`, we want to allow multiple assertion failures to be collected and reported at once: ```csharp @@ -25,9 +23,11 @@ using (Assert.Scope()) // Dispose() throws AggregateException-like AssertFailedException with all 3 failures ``` -### The nullability problem +## Technical challenges + +Soft assertions create a fundamental tension with C# nullability annotations and, more broadly, with all assertion postconditions. -Before soft assertions, `ThrowAssertFailed` was annotated with `[DoesNotReturn]`, which let the compiler prove post-condition contracts. For example: +Before soft assertions, `ReportAssertFailed` was annotated with `[DoesNotReturn]`, which let the compiler prove post-condition contracts. For example: ```csharp public static void IsNotNull([NotNull] object? value, ...) @@ -40,7 +40,7 @@ public static void IsNotNull([NotNull] object? value, ...) } ``` -To support soft assertions, `ThrowAssertFailed` was changed so it no longer always throws — within a scope, it adds the failure to a queue and *returns*. This means: +To support soft assertions, `ReportAssertFailed` was changed so it no longer always throws — within a scope, it adds the failure to a queue and *returns*. This means: 1. `[DoesNotReturn]` can no longer be applied to the general failure path. 2. `[DoesNotReturnIf(false)]` on `IsTrue` / `[DoesNotReturnIf(true)]` on `IsFalse` become lies — the method can return even when the condition is not met. @@ -48,6 +48,18 @@ To support soft assertions, `ThrowAssertFailed` was changed so it no longer alwa If we lie about these annotations, **downstream code after the assertion will get wrong nullability analysis**, potentially causing `NullReferenceException` at runtime with no compiler warning. +After discussion with the Roslyn team, a key insight emerged: **this is a general problem with postconditions, not specific to nullability**. `Assert.Scope()` means assertions are no longer enforcing *any* postconditions. Consider: + +```csharp +using (Assert.Scope()) +{ + Assert.AreEqual("blah", item.Prop); + MyTestHelper(item.Prop); // may explode if Prop doesn't have expected form +} +``` + +`Assert.AreEqual` in scoped mode already does not enforce its postcondition (that the values are equal). Code after the assertion may use `item.Prop` assuming it has a particular value, and that assumption may be wrong. The nullability case (`IsNotNull` not guaranteeing non-null) is conceptually identical — it's just another postcondition that isn't enforced within a scope. + ## Options Considered ### Option 1: Remove all nullability annotations @@ -59,16 +71,7 @@ Remove `[DoesNotReturn]`, `[DoesNotReturnIf]`, and `[NotNull]` from all assertio **Verdict:** Rejected. Too disruptive for all users, including those who never use `Assert.Scope()`. -### Option 2: Keep all annotations, suppress all warnings - -Keep `[DoesNotReturn]`, `[DoesNotReturnIf]`, `[NotNull]` on everything, blanket-suppress CS8777/CS8763. - -**Pros:** No user-facing changes. Code compiles cleanly. -**Cons:** The annotations are lies inside a scope. `Assert.IsNotNull(obj)` inside a scope won't throw, meaning `obj` could still be null on the next line, but the compiler thinks it's non-null. This trades a visible assertion failure for a hidden `NullReferenceException`. - -**Verdict:** Rejected. Lying about type-narrowing annotations (`[NotNull]`) is actively dangerous — it causes runtime crashes. - -### Option 3: Pragmatic tier split (chosen) +### Option 2: Pragmatic tier split Categorize assertions into tiers based on whether their post-conditions narrow types, and handle each tier differently. @@ -87,76 +90,68 @@ Categorize assertions into tiers based on whether their post-conditions narrow t **Tier 3 — Soft, no annotation impact:** All other assertions that don't carry type-narrowing annotations. These become fully soft within a scope. -- `AreEqual`, `AreNotEqual`, `AreSame`, `AreNotSame` -- `Inconclusive` -- `Contains`, `DoesNotContain` -- `IsNull`, `IsNotInstanceOfType`, `IsNotExactInstanceOfType` -- `StartsWith`, `EndsWith`, `Matches`, `DoesNotMatch` -- `IsGreaterThan`, `IsLessThan`, etc. -- `ThrowsException`, `ThrowsExactException` -- All `StringAssert.*` and `CollectionAssert.*` methods +**Pros:** Type-narrowing contracts are always truthful. Soft assertions work for the vast majority of assertions. +**Cons:** `IsNotNull` / `IsInstanceOfType` / `IsExactInstanceOfType` won't participate in soft assertion collection — they still throw immediately within a scope. This significantly reduces the value of `Assert.Scope()` for common test patterns like null-checking multiple properties. Users lose `[DoesNotReturnIf]` narrowing on `IsTrue`/`IsFalse` even outside scopes. -**Pros:** Type-narrowing contracts are always truthful. Soft assertions work for the vast majority of assertions. The few assertions that must remain hard are exactly the ones where continuing would cause crashes. -**Cons:** `IsNotNull` / `IsInstanceOfType` / `IsExactInstanceOfType` won't participate in soft assertion collection — they still throw immediately within a scope. +**Verdict:** Rejected. Carving out exceptions makes the scoping feature less useful, and the safety benefit is questionable given that *all* postconditions (not just nullability ones) are already unenforced in scoped mode. -**Verdict:** Chosen. This is the only option that is both honest to the compiler and safe at runtime. +### Option 3: Keep all annotations, suppress compiler warnings (chosen) -### Option 3a: Sub-exception for precondition failures +Keep `[DoesNotReturn]`, `[DoesNotReturnIf]`, `[NotNull]` on all assertion methods. Make all assertions soft within a scope (except `Assert.Fail()` and `CheckParameterNotNull`). Suppress `#pragma warning disable CS8777` / `CS8763` where the compiler objects. -A variant considered was introducing `internal AssertPreconditionFailedException : AssertFailedException` to distinguish hard failures from soft ones, enabling different handling in the adapter pipeline. +This is the approach recommended by the Roslyn team: leave all nullable attributes on, but do not actually ensure any of the postconditions when in `Assert.Scope()` context. This is consistent with what we are already doing for all postconditions unrelated to nullability — `Assert.AreEqual` doesn't guarantee equality in scoped mode, `Assert.IsTrue` doesn't guarantee the condition was true, and so on. The nullability annotations are no different. -**Verdict:** Rejected. The existing adapter pipeline checks `is AssertFailedException` in multiple places (`ExceptionExtensions.TryGetUnitTestAssertException`, `TestClassInfo`, `TestMethodInfo`, etc.). A sub-exception would still match these checks. Adding a new exception type adds complexity without clear benefit, and risks breaking extensibility points that pattern-match on `AssertFailedException`. +**Pros:** -## Chosen Design: Two Internal Methods +- **No user-facing annotation changes.** Users outside `Assert.Scope()` get the exact same experience — `Assert.IsNotNull(obj); obj.Method()` has no nullable warning, `Assert.IsTrue(b)` narrows `bool?` to `bool`. Zero regression. +- **All assertions participate in soft collection.** `IsNotNull`, `IsInstanceOfType`, `IsExactInstanceOfType`, `ContainsSingle`, `IsTrue`, `IsFalse` are all soft within a scope. This maximizes the value of `Assert.Scope()`. +- **Consistent mental model.** The rule is simple: within `Assert.Scope()`, assertion failures are collected and postconditions are not enforced. This applies uniformly to all assertions (except `Assert.Fail()`), whether the postcondition is about nullability, type narrowing, equality, or anything else. -### `ReportHardAssertFailure` +**Cons:** -```csharp -[DoesNotReturn] -[StackTraceHidden] -internal static void ReportHardAssertFailure(string assertionName, string? message) -``` +- **The annotations are lies inside a scope.** `Assert.IsNotNull(obj)` inside a scope won't throw when `obj` is null, meaning `obj` could still be null on the next line, but the compiler thinks it's non-null. This can cause `NullReferenceException` at runtime with no compiler warning. +- **Requires `#pragma warning disable` to suppress CS8777/CS8763.** The compiler correctly identifies that our implementation doesn't fulfill the annotation promises in all code paths. + +The runtime risk is acceptable for the same reason that non-nullability postconditions being unenforced is acceptable: the assertion *will* be reported as failed when the scope disposes. The user will see the failure. If downstream code crashes due to a violated postcondition (whether it's a `NullReferenceException` from a null value, or some other error from an unexpected value), that crash is a secondary symptom of the already-reported assertion failure — not a silent, hidden bug. -- **Always throws**, even within an `AssertScope`. -- Carries `[DoesNotReturn]` — compiler can trust post-conditions. -- **Launches the debugger** if configured (`DebuggerLaunchMode.Enabled` / `EnabledExcludingCI`). -- Used by: Tier 1 assertions (`IsNotNull`, `IsInstanceOfType`, `IsExactInstanceOfType`, `Fail`, `ContainsSingle`), `CheckParameterNotNull`, `AssertScope.Dispose()`. +Users who need a postcondition to be enforced for subsequent code to work correctly can use `Assert.Fail()` (which always throws) or restructure their test to not depend on the postcondition after the assertion within a scope. -### `ReportSoftAssertFailure` +**Verdict:** Chosen. This approach gives the best user experience both inside and outside `Assert.Scope()`, and is consistent with how all other postconditions already behave in scoped mode. + +## Detailed Design + +This section describes the implementation of Option 3 (keep all annotations, suppress compiler warnings), which centers on a single `ReportAssertFailed` method that switches behavior based on whether an `AssertScope` is active. + +### `ReportAssertFailed` ```csharp [StackTraceHidden] -internal static void ReportSoftAssertFailure(string assertionName, string? message) +internal static void ReportAssertFailed(string assertionName, string? message) ``` - Within an `AssertScope`: adds failure to the scope's queue and **returns**. - Outside a scope: **throws** `AssertFailedException` (preserves existing behavior). -- **No `[DoesNotReturn]`** — compiler knows the method can return. -- **Does not launch the debugger** — the debugger is triggered later when `AssertScope.Dispose()` calls `ReportHardAssertFailure`. -- Used by: Tier 2 and Tier 3 assertions. ### `AssertScope.Dispose()` When an `AssertScope` is disposed and it contains collected failures: -- **Single failure:** Calls `ReportHardAssertFailure(singleError)` — this throws the original `AssertFailedException` and triggers the debugger. -- **Multiple failures:** Calls `ReportHardAssertFailure(new AssertFailedException(combinedMessage, new AggregateException(allErrors)))` — wraps all collected failures into an `AggregateException` as the inner exception. +- **Single failure:** Throws the original `AssertFailedException` and triggers the debugger. +- **Multiple failures:** Throws a new `AssertFailedException` wrapping all collected failures into an `AggregateException` as the inner exception. This design ensures the debugger breaks at the point where the scope is disposed, giving the developer visibility into all collected failures. -### `CheckParameterNotNull` +### Nullable annotations: kept but unenforced in scoped mode -The internal helper `CheckParameterNotNull` validates that assertion *parameters* (not the values under test) are non-null. For example, validating that a `Type` argument passed to `IsInstanceOfType` is not null. +All nullable annotations (`[NotNull]`, `[DoesNotReturnIf]`) are kept on their respective assertion methods. Within a scope, these postconditions are not enforced — the method may return without the postcondition being true. Compiler warnings (CS8777, CS8763) arising from this are suppressed with `#pragma warning disable`. -This uses `ReportHardAssertFailure` because: +This is the same approach the Roslyn team recommended. As Rikki from the Roslyn team noted: -1. A null parameter is a test authoring bug, not a test value failure. -2. It would be confusing to silently collect a "your parameter was null" error alongside real assertion results. -3. It preserves the existing behavior of throwing `AssertFailedException` (not `ArgumentNullException`), which avoids breaking the adapter pipeline that maps exception types to test outcomes. +> It feels like this is an issue with postconditions in general... Assert.Scoped() means assertions are no longer enforcing postconditions. [...] I would honestly start by just trying leaving all the nullable attributes on, but not actually ensuring any of the postconditions, when in Assert.Scoped() context. Since that is essentially what you are doing already with all postconditions unrelated to nullability. See how that works in practice, and, if the usability feels bad, you could consider introducing certain assertions that throw regardless of whether you're in scoped context or not. ### `Assert.Fail()` — hard by design -`Assert.Fail()` is a Tier 1 hard assertion. It calls `ReportHardAssertFailure` and always throws, even within a scope. This is the correct choice for two reasons: +`Assert.Fail()` is the only assertion that always throws, even within a scope. It inlines its throw logic (bypassing `ReportAssertFailed`) for two reasons: 1. **Semantics:** `Fail()` means "this test has unconditionally failed." There is no meaningful scenario where you'd want to collect a `Fail()` and keep executing — the developer explicitly declared the test a failure. 2. **Public API contract:** `Assert.Fail()` is annotated `[DoesNotReturn]`, and users rely on this for control flow: @@ -176,31 +171,56 @@ Making `Fail()` hard keeps the `[DoesNotReturn]` annotation truthful with no pra ### No `Assert.Scope()` — no change -Users who don't use `Assert.Scope()` experience **zero behavioral change**. All assertions throw exactly as before. The only user-visible annotation change is the removal of `[DoesNotReturnIf]` from `IsTrue`/`IsFalse`, which means the compiler will no longer narrow `bool?` to `bool` after these calls (a minor regression affecting a niche pattern). +Users who don't use `Assert.Scope()` experience **zero behavioral change**. All assertions throw exactly as before. All nullable annotations remain in place. There is no regression. ### Within `Assert.Scope()` -| Assertion | Behavior | -| --------- | -------- | -| `IsNotNull`, `IsInstanceOfType`, `IsExactInstanceOfType` | Always throws immediately (hard). These assertions narrow types and cannot safely be deferred. | -| `Assert.Fail()` | Always throws immediately (hard). Semantically means unconditional failure — no reason to defer. | -| `Assert.ContainsSingle()` | Always throws immediately (hard). Returns the matched element — returning `default` in soft mode would give callers a bogus value. | -| `IsTrue`, `IsFalse` | Soft. Failures collected. `[DoesNotReturnIf]` removed. | -| All other assertions | Soft. Failures collected. | +All assertions participate in soft failure collection, with the following exceptions: + +- **`Assert.Fail()`** is the only assertion API that does not respect soft failure mode. It always throws immediately, even within a scope, because it semantically means "this test has unconditionally failed" — there is no reason to defer. +- **Null precondition checks** inside Assert APIs (e.g., validating that a `Type` argument passed to `IsInstanceOfType` is not null) also throw directly rather than collecting. These are internal parameter validation checks (`CheckParameterNotNull`), not assertions on the value under test. Note that `Assert.IsNotNull` / `Assert.IsNull` are *not* precondition checks — they are assertions on test values and participate in soft collection normally. + +### Dealing with postcondition-dependent code in scoped mode + +When using `Assert.Scope()`, code after an assertion should not depend on the assertion's postcondition. This applies to all postconditions, whether nullability-related or not: + +```csharp +using (Assert.Scope()) +{ + Assert.IsNotNull(item); + // item might still be null here — the assertion failure was collected, not thrown. + // If you need item to be non-null for the rest of the test, use Assert.Fail() + // or restructure the test. + + Assert.AreEqual("blah", item.Prop); + MyTestHelper(item.Prop); + // item.Prop might not be "blah" — same issue, different postcondition. +} +``` + +If a test helper depends on a postcondition being true, the user has several options: + +1. **Use `Assert.Fail()` for critical preconditions** — it always throws, even in scoped mode. +2. **Restructure the test** to not depend on postconditions within the scope. +3. **Accept the secondary failure** — the primary assertion failure will be reported, and any downstream crash is a secondary symptom. + +This is simply part of the adoption/onboarding cost of using `Assert.Scope()`. The scoping feature trades strict postcondition enforcement for the ability to see multiple failures at once. ## Design Decisions -### `IsTrue` / `IsFalse` are Tier 2 (soft, annotations removed) +### Why lying to the compiler is acceptable -`IsTrue` had `[DoesNotReturnIf(false)]` and `IsFalse` had `[DoesNotReturnIf(true)]`. These annotations let the compiler narrow `bool?` to `bool` after the call. By making these assertions soft, we had to remove the annotations — the compiler can no longer assume the condition held. +The decision to keep annotations that are not enforced in scoped mode is justified by: -This was deemed acceptable because: +1. **Consistency.** All assertion postconditions are already unenforced in scoped mode. Making nullability postconditions the exception adds complexity without meaningful safety improvement. +2. **User experience.** Removing annotations would regress the experience for all users, including those who never use `Assert.Scope()`. +3. **Practicality.** The Roslyn team confirmed this approach is reasonable. Tests that use `Assert.Scope()` are inherently opting into a mode where postconditions are deferred, and users should expect that downstream code may encounter unexpected state. +4. **Observable failures.** The violated postcondition doesn't cause silent bugs — the assertion failure *is* reported when the scope disposes. Any secondary crash is additional evidence of the already-reported failure. +5. **Experimental API.** The `Assert.Scope()` API is currently marked as experimental, which allows us to gather concrete usages and feedback from users before committing to a stable release. Real-world usage patterns will inform whether any adjustments to the annotation strategy or scoping behavior are needed, and we can iterate on the design without breaking stable API contracts. -- The narrowing only affects `bool?` → `bool`, not reference types. The risk of a downstream `NullReferenceException` does not apply. -- The pattern of using `Assert.IsTrue` to narrow a nullable boolean is niche. Most callers pass a plain `bool`. -- Keeping these as hard assertions would significantly reduce the value of `Assert.Scope()`, since `IsTrue`/`IsFalse` are among the most commonly used assertions. +### Why nested scopes are not supported -This decision can be reconsidered if the annotation loss proves more impactful than expected. +Nested `Assert.Scope()` calls are currently not allowed. We do not see a compelling usage scenario that justifies the added complexity of defining nested scope semantics (e.g., should inner scope failures propagate to the parent scope or throw immediately?). This decision can be revisited based on customer feedback if concrete use cases emerge. ## Future Improvements @@ -217,29 +237,7 @@ using (Assert.Scope()) } ``` -The exact shape of this API is not yet designed. - -### Nested scopes - -Currently, `AssertScope` uses `AsyncLocal` and supports a single active scope. Nested scopes could allow finer-grained grouping of assertion failures: - -```csharp -using (Assert.Scope()) -{ - Assert.AreEqual(1, actual.X); - - using (Assert.Scope()) - { - Assert.AreEqual(2, actual.Y); - Assert.AreEqual(3, actual.Z); - } - // Inner scope disposes here — should inner failures propagate to outer scope or throw? - - Assert.AreEqual(4, actual.W); -} -``` - -The semantics of inner scope disposal (propagate to parent vs. throw immediately) need to be defined. +The exact shape of this API is not yet designed. As the Roslyn team suggested, users may want certain assertions to always throw so they can enforce postconditions that subsequent code depends on, even within a scope. This would be part of the natural evolution of the feature based on real-world usage feedback. ### Extensibility for custom assertion authors @@ -260,4 +258,4 @@ public static class MyCustomAssertions } ``` -This would require promoting some form of the `ReportSoftAssertFailure` / `ReportHardAssertFailure` API from `internal` to `public`, with careful API design to avoid exposing implementation details. +This would require promoting `ReportAssertFailed` (or a new public variant) from `internal` to `public`, with careful API design to avoid exposing implementation details. From 61899b1400872ef96fd24c3b6acd55a97b1e0b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 12 Feb 2026 14:25:57 +0100 Subject: [PATCH 10/17] Add back removed NotNull attributes --- .../Assertions/Assert.IsExactInstanceOfType.cs | 6 ++++-- .../TestFramework/Assertions/Assert.IsInstanceOfType.cs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index 103eb2fb89..02810ded5f 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -369,7 +369,7 @@ private static void ThrowAssertIsExactInstanceOfTypeFailed(object? value, Type? /// is exactly the type /// of . /// - public static void IsNotExactInstanceOfType(object? value, Type? wrongType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") + public static void IsNotExactInstanceOfType(object? value, [NotNull] Type? wrongType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsNotExactInstanceOfTypeFailing(value, wrongType)) { @@ -379,9 +379,11 @@ public static void IsNotExactInstanceOfType(object? value, Type? wrongType, stri /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 - public static void IsNotExactInstanceOfType(object? value, Type? wrongType, [InterpolatedStringHandlerArgument(nameof(value), nameof(wrongType))] ref AssertIsNotExactInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") + public static void IsNotExactInstanceOfType(object? value, [NotNull] Type? wrongType, [InterpolatedStringHandlerArgument(nameof(value), nameof(wrongType))] ref AssertIsNotExactInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. => message.ComputeAssertion(valueExpression); +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// /// Tests whether the specified object is not exactly an instance of the wrong generic diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index 9675a9601d..4c46c269a5 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -372,7 +372,7 @@ private static void ThrowAssertIsInstanceOfTypeFailed(object? value, Type? expec /// is in the inheritance hierarchy /// of . /// - public static void IsNotInstanceOfType(object? value, Type? wrongType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") + public static void IsNotInstanceOfType(object? value, [NotNull] Type? wrongType, string? message = "", [CallerArgumentExpression(nameof(value))] string valueExpression = "") { if (IsNotInstanceOfTypeFailing(value, wrongType)) { @@ -382,9 +382,11 @@ public static void IsNotInstanceOfType(object? value, Type? wrongType, string? m /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 - public static void IsNotInstanceOfType(object? value, Type? wrongType, [InterpolatedStringHandlerArgument(nameof(value), nameof(wrongType))] ref AssertIsNotInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") + public static void IsNotInstanceOfType(object? value, [NotNull] Type? wrongType, [InterpolatedStringHandlerArgument(nameof(value), nameof(wrongType))] ref AssertIsNotInstanceOfTypeInterpolatedStringHandler message, [CallerArgumentExpression(nameof(value))] string valueExpression = "") #pragma warning restore IDE0060 // Remove unused parameter +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - Not sure how to express the semantics to the compiler, but the implementation guarantees that. => message.ComputeAssertion(valueExpression); +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// /// Tests whether the specified object is not an instance of the wrong generic From bb40e99b1fa026542df9abb2dcdd4c5f61d605c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 16 Feb 2026 10:08:50 +0100 Subject: [PATCH 11/17] Update src/TestFramework/TestFramework/Assertions/Assert.cs Co-authored-by: Youssef Victor --- src/TestFramework/TestFramework/Assertions/Assert.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 36f3aa6327..de2886c107 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -44,14 +44,10 @@ private Assert() internal static void ReportAssertFailed(string assertionName, string? message, bool forceThrow = false) { var assertionFailedException = new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); - if (!forceThrow) + if (!forceThrow && AssertScope.Current is { } scope) { - AssertScope? scope = AssertScope.Current; - if (scope is not null) - { - scope.AddError(assertionFailedException); - return; - } + scope.AddError(assertionFailedException); + return; } ThrowAssertFailed(assertionFailedException); From 328b1fbbcfdb307b17e018ef8b61186af94d0c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 16 Feb 2026 12:10:23 +0100 Subject: [PATCH 12/17] Address review comments --- .../011-Soft-Assertions-Nullability-Design.md | 22 +++---- .../Assertions/Assert.AreEqual.cs | 58 +++++++++---------- .../Assertions/Assert.AreSame.cs | 12 ++-- .../Assertions/Assert.Contains.cs | 48 +++++++-------- .../TestFramework/Assertions/Assert.Count.cs | 14 ++--- .../TestFramework/Assertions/Assert.Fail.cs | 4 +- .../Assertions/Assert.IComparable.cs | 32 +++++----- .../Assertions/Assert.Inconclusive.cs | 2 +- .../Assert.IsExactInstanceOfType.cs | 18 +++--- .../Assertions/Assert.IsInstanceOfType.cs | 18 +++--- .../TestFramework/Assertions/Assert.IsNull.cs | 14 ++--- .../TestFramework/Assertions/Assert.IsTrue.cs | 12 ++-- .../TestFramework/Assertions/Assert.That.cs | 5 +- .../TestFramework/Assertions/Assert.cs | 58 +++++++++++-------- .../TestFramework/Assertions/AssertScope.cs | 9 ++- .../Resources/FrameworkMessages.resx | 4 -- .../Resources/xlf/FrameworkMessages.cs.xlf | 5 -- .../Resources/xlf/FrameworkMessages.de.xlf | 5 -- .../Resources/xlf/FrameworkMessages.es.xlf | 5 -- .../Resources/xlf/FrameworkMessages.fr.xlf | 5 -- .../Resources/xlf/FrameworkMessages.it.xlf | 5 -- .../Resources/xlf/FrameworkMessages.ja.xlf | 5 -- .../Resources/xlf/FrameworkMessages.ko.xlf | 5 -- .../Resources/xlf/FrameworkMessages.pl.xlf | 5 -- .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 5 -- .../Resources/xlf/FrameworkMessages.ru.xlf | 5 -- .../Resources/xlf/FrameworkMessages.tr.xlf | 5 -- .../xlf/FrameworkMessages.zh-Hans.xlf | 5 -- .../xlf/FrameworkMessages.zh-Hant.xlf | 5 -- 29 files changed, 170 insertions(+), 225 deletions(-) diff --git a/docs/RFCs/011-Soft-Assertions-Nullability-Design.md b/docs/RFCs/011-Soft-Assertions-Nullability-Design.md index f9b60a2f1a..2696229e16 100644 --- a/docs/RFCs/011-Soft-Assertions-Nullability-Design.md +++ b/docs/RFCs/011-Soft-Assertions-Nullability-Design.md @@ -81,6 +81,7 @@ Categorize assertions into tiers based on whether their post-conditions narrow t - `IsInstanceOfType` — annotated `[NotNull]` on the value parameter - `IsExactInstanceOfType` — annotated `[NotNull]` on the value parameter - `Fail` — semantically means "unconditional failure"; annotated `[DoesNotReturn]` on public API +- `Inconclusive` — semantically means "unconditional inconclusive"; annotated `[DoesNotReturn]` on public API and throws `AssertInconclusiveException` (not `AssertFailedException`) - `ContainsSingle` — returns the matched element; returning `default` in soft mode would give callers a bogus `null`/`default(T)` causing downstream errors **Tier 2 — Soft, but annotations removed:** Assertions that had conditional `[DoesNotReturnIf]` annotations. The annotation is removed so the compiler no longer assumes the condition is guaranteed. The assertions become soft (collected within a scope). @@ -97,7 +98,7 @@ Categorize assertions into tiers based on whether their post-conditions narrow t ### Option 3: Keep all annotations, suppress compiler warnings (chosen) -Keep `[DoesNotReturn]`, `[DoesNotReturnIf]`, `[NotNull]` on all assertion methods. Make all assertions soft within a scope (except `Assert.Fail()` and `CheckParameterNotNull`). Suppress `#pragma warning disable CS8777` / `CS8763` where the compiler objects. +Keep `[DoesNotReturn]`, `[DoesNotReturnIf]`, `[NotNull]` on all assertion methods. Make all assertions soft within a scope (except `Assert.Fail()`, `Assert.Inconclusive()`, and `CheckParameterNotNull`). Suppress `#pragma warning disable CS8777` / `CS8763` where the compiler objects. This is the approach recommended by the Roslyn team: leave all nullable attributes on, but do not actually ensure any of the postconditions when in `Assert.Scope()` context. This is consistent with what we are already doing for all postconditions unrelated to nullability — `Assert.AreEqual` doesn't guarantee equality in scoped mode, `Assert.IsTrue` doesn't guarantee the condition was true, and so on. The nullability annotations are no different. @@ -105,7 +106,7 @@ This is the approach recommended by the Roslyn team: leave all nullable attribut - **No user-facing annotation changes.** Users outside `Assert.Scope()` get the exact same experience — `Assert.IsNotNull(obj); obj.Method()` has no nullable warning, `Assert.IsTrue(b)` narrows `bool?` to `bool`. Zero regression. - **All assertions participate in soft collection.** `IsNotNull`, `IsInstanceOfType`, `IsExactInstanceOfType`, `ContainsSingle`, `IsTrue`, `IsFalse` are all soft within a scope. This maximizes the value of `Assert.Scope()`. -- **Consistent mental model.** The rule is simple: within `Assert.Scope()`, assertion failures are collected and postconditions are not enforced. This applies uniformly to all assertions (except `Assert.Fail()`), whether the postcondition is about nullability, type narrowing, equality, or anything else. +- **Consistent mental model.** The rule is simple: within `Assert.Scope()`, assertion failures are collected and postconditions are not enforced. This applies uniformly to all assertions (except `Assert.Fail()` and `Assert.Inconclusive()`), whether the postcondition is about nullability, type narrowing, equality, or anything else. **Cons:** @@ -114,7 +115,7 @@ This is the approach recommended by the Roslyn team: leave all nullable attribut The runtime risk is acceptable for the same reason that non-nullability postconditions being unenforced is acceptable: the assertion *will* be reported as failed when the scope disposes. The user will see the failure. If downstream code crashes due to a violated postcondition (whether it's a `NullReferenceException` from a null value, or some other error from an unexpected value), that crash is a secondary symptom of the already-reported assertion failure — not a silent, hidden bug. -Users who need a postcondition to be enforced for subsequent code to work correctly can use `Assert.Fail()` (which always throws) or restructure their test to not depend on the postcondition after the assertion within a scope. +Users who need a postcondition to be enforced for subsequent code to work correctly can use `Assert.Fail()` or `Assert.Inconclusive()` (which always throw) or restructure their test to not depend on the postcondition after the assertion within a scope. **Verdict:** Chosen. This approach gives the best user experience both inside and outside `Assert.Scope()`, and is consistent with how all other postconditions already behave in scoped mode. @@ -149,12 +150,13 @@ This is the same approach the Roslyn team recommended. As Rikki from the Roslyn > It feels like this is an issue with postconditions in general... Assert.Scoped() means assertions are no longer enforcing postconditions. [...] I would honestly start by just trying leaving all the nullable attributes on, but not actually ensuring any of the postconditions, when in Assert.Scoped() context. Since that is essentially what you are doing already with all postconditions unrelated to nullability. See how that works in practice, and, if the usability feels bad, you could consider introducing certain assertions that throw regardless of whether you're in scoped context or not. -### `Assert.Fail()` — hard by design +### `Assert.Fail()` and `Assert.Inconclusive()` — hard by design -`Assert.Fail()` is the only assertion that always throws, even within a scope. It inlines its throw logic (bypassing `ReportAssertFailed`) for two reasons: +`Assert.Fail()` and `Assert.Inconclusive()` are the only assertions that always throw, even within a scope. They bypass `ReportAssertFailed` for these reasons: -1. **Semantics:** `Fail()` means "this test has unconditionally failed." There is no meaningful scenario where you'd want to collect a `Fail()` and keep executing — the developer explicitly declared the test a failure. -2. **Public API contract:** `Assert.Fail()` is annotated `[DoesNotReturn]`, and users rely on this for control flow: +1. **Semantics:** `Fail()` means "this test has unconditionally failed" and `Inconclusive()` means "this test cannot determine its result." There is no meaningful scenario where you'd want to collect these and keep executing — the developer explicitly declared the test outcome. +2. **Exception types:** `Assert.Fail()` throws `AssertFailedException` while `Assert.Inconclusive()` throws `AssertInconclusiveException`. The test runner relies on the exception type to distinguish between failed and inconclusive outcomes. Routing `Inconclusive()` through `ReportAssertFailed` would also lose this distinction, since the scope's collection queue only holds `AssertFailedException`. +3. **Public API contract:** Both are annotated `[DoesNotReturn]`, and users rely on this for control flow: ```csharp var result = condition switch @@ -165,7 +167,7 @@ var result = condition switch }; ``` -Making `Fail()` hard keeps the `[DoesNotReturn]` annotation truthful with no pragma suppression needed. +Making `Fail()` and `Inconclusive()` hard keeps the `[DoesNotReturn]` annotation truthful with no pragma suppression needed. ## Impact on Users @@ -177,7 +179,7 @@ Users who don't use `Assert.Scope()` experience **zero behavioral change**. All All assertions participate in soft failure collection, with the following exceptions: -- **`Assert.Fail()`** is the only assertion API that does not respect soft failure mode. It always throws immediately, even within a scope, because it semantically means "this test has unconditionally failed" — there is no reason to defer. +- **`Assert.Fail()`** and **`Assert.Inconclusive()`** are the only assertion APIs that do not respect soft failure mode. They always throw immediately, even within a scope — `Fail()` because it semantically means "this test has unconditionally failed" and `Inconclusive()` because it means "this test cannot determine its result." Additionally, `Inconclusive()` throws `AssertInconclusiveException` (not `AssertFailedException`), so it cannot be collected in the scope's failure queue. - **Null precondition checks** inside Assert APIs (e.g., validating that a `Type` argument passed to `IsInstanceOfType` is not null) also throw directly rather than collecting. These are internal parameter validation checks (`CheckParameterNotNull`), not assertions on the value under test. Note that `Assert.IsNotNull` / `Assert.IsNull` are *not* precondition checks — they are assertions on test values and participate in soft collection normally. ### Dealing with postcondition-dependent code in scoped mode @@ -200,7 +202,7 @@ using (Assert.Scope()) If a test helper depends on a postcondition being true, the user has several options: -1. **Use `Assert.Fail()` for critical preconditions** — it always throws, even in scoped mode. +1. **Use `Assert.Fail()` or `Assert.Inconclusive()` for critical preconditions** — they always throw, even in scoped mode. 2. **Restructure the test** to not depend on postconditions within the scope. 3. **Accept the secondary failure** — the primary assertion failure will be reported, and any downstream crash is a secondary symptom. diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 65146931b3..c7f7a27d70 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -49,7 +49,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "expected", expectedExpression, "actual", actualExpression) + " "); - ThrowAssertAreEqualFailed(_expected, _actual, _builder.ToString()); + ReportAssertAreEqualFailed(_expected, _actual, _builder.ToString()); } } @@ -116,7 +116,7 @@ internal void ComputeAssertion(string notExpectedExpression, string actualExpres if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "notExpected", notExpectedExpression, "actual", actualExpression) + " "); - ThrowAssertAreNotEqualFailed(_notExpected, _actual, _builder.ToString()); + ReportAssertAreNotEqualFailed(_notExpected, _actual, _builder.ToString()); } } @@ -167,7 +167,7 @@ public AssertNonGenericAreEqualInterpolatedStringHandler(int literalLength, int if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreEqualFailed(expected, actual, delta, userMessage); + _failAction = userMessage => ReportAssertAreEqualFailed(expected, actual, delta, userMessage); } } @@ -177,7 +177,7 @@ public AssertNonGenericAreEqualInterpolatedStringHandler(int literalLength, int if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreEqualFailed(expected, actual, delta, userMessage); + _failAction = userMessage => ReportAssertAreEqualFailed(expected, actual, delta, userMessage); } } @@ -187,7 +187,7 @@ public AssertNonGenericAreEqualInterpolatedStringHandler(int literalLength, int if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreEqualFailed(expected, actual, delta, userMessage); + _failAction = userMessage => ReportAssertAreEqualFailed(expected, actual, delta, userMessage); } } @@ -197,7 +197,7 @@ public AssertNonGenericAreEqualInterpolatedStringHandler(int literalLength, int if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreEqualFailed(expected, actual, delta, userMessage); + _failAction = userMessage => ReportAssertAreEqualFailed(expected, actual, delta, userMessage); } } @@ -213,7 +213,7 @@ public AssertNonGenericAreEqualInterpolatedStringHandler(int literalLength, int if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreEqualFailed(expected, actual, ignoreCase, culture, userMessage); + _failAction = userMessage => ReportAssertAreEqualFailed(expected, actual, ignoreCase, culture, userMessage); } } @@ -273,7 +273,7 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); } } @@ -283,7 +283,7 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); } } @@ -293,7 +293,7 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); } } @@ -303,7 +303,7 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); } } @@ -319,7 +319,7 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ThrowAssertAreNotEqualFailed(notExpected, actual, userMessage); + _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, userMessage); } } @@ -489,7 +489,7 @@ public static void AreEqual(T? expected, T? actual, IEqualityComparer comp } string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ThrowAssertAreEqualFailed(expected, actual, userMessage); + ReportAssertAreEqualFailed(expected, actual, userMessage); } private static bool AreEqualFailing(T? expected, T? actual, IEqualityComparer? comparer) @@ -643,7 +643,7 @@ private static string FormatStringDifferenceMessage(string expected, string actu } [DoesNotReturn] - private static void ThrowAssertAreEqualFailed(object? expected, object? actual, string userMessage) + private static void ReportAssertAreEqualFailed(object? expected, object? actual, string userMessage) { string finalMessage = actual != null && expected != null && !actual.GetType().Equals(expected.GetType()) ? string.Format( @@ -666,7 +666,7 @@ private static void ThrowAssertAreEqualFailed(object? expected, object? actual, } [DoesNotReturn] - private static void ThrowAssertAreEqualFailed(T expected, T actual, T delta, string userMessage) + private static void ReportAssertAreEqualFailed(T expected, T actual, T delta, string userMessage) where T : struct, IConvertible { string finalMessage = string.Format( @@ -680,7 +680,7 @@ private static void ThrowAssertAreEqualFailed(T expected, T actual, T delta, } [DoesNotReturn] - private static void ThrowAssertAreEqualFailed(string? expected, string? actual, bool ignoreCase, CultureInfo culture, string userMessage) + private static void ReportAssertAreEqualFailed(string? expected, string? actual, bool ignoreCase, CultureInfo culture, string userMessage) { string finalMessage; @@ -792,7 +792,7 @@ public static void AreNotEqual(T? notExpected, T? actual, IEqualityComparer @@ -838,7 +838,7 @@ public static void AreEqual(float expected, float actual, float delta, string? m if (AreEqualFailing(expected, actual, delta)) { string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ThrowAssertAreEqualFailed(expected, actual, delta, userMessage); + ReportAssertAreEqualFailed(expected, actual, delta, userMessage); } } @@ -885,7 +885,7 @@ public static void AreNotEqual(float notExpected, float actual, float delta, str if (AreNotEqualFailing(notExpected, actual, delta)) { string userMessage = BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression); - ThrowAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); } } @@ -953,7 +953,7 @@ public static void AreEqual(decimal expected, decimal actual, decimal delta, str if (AreEqualFailing(expected, actual, delta)) { string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ThrowAssertAreEqualFailed(expected, actual, delta, userMessage); + ReportAssertAreEqualFailed(expected, actual, delta, userMessage); } } @@ -1000,7 +1000,7 @@ public static void AreNotEqual(decimal notExpected, decimal actual, decimal delt if (AreNotEqualFailing(notExpected, actual, delta)) { string userMessage = BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression); - ThrowAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); } } @@ -1050,7 +1050,7 @@ public static void AreEqual(long expected, long actual, long delta, string? mess if (AreEqualFailing(expected, actual, delta)) { string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ThrowAssertAreEqualFailed(expected, actual, delta, userMessage); + ReportAssertAreEqualFailed(expected, actual, delta, userMessage); } } @@ -1097,7 +1097,7 @@ public static void AreNotEqual(long notExpected, long actual, long delta, string if (AreNotEqualFailing(notExpected, actual, delta)) { string userMessage = BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression); - ThrowAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); } } @@ -1146,7 +1146,7 @@ public static void AreEqual(double expected, double actual, double delta, string if (AreEqualFailing(expected, actual, delta)) { string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ThrowAssertAreEqualFailed(expected, actual, delta, userMessage); + ReportAssertAreEqualFailed(expected, actual, delta, userMessage); } } @@ -1193,7 +1193,7 @@ public static void AreNotEqual(double notExpected, double actual, double delta, if (AreNotEqualFailing(notExpected, actual, delta)) { string userMessage = BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression); - ThrowAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); } } @@ -1219,7 +1219,7 @@ private static bool AreNotEqualFailing(double notExpected, double actual, double } [DoesNotReturn] - private static void ThrowAssertAreNotEqualFailed(T notExpected, T actual, T delta, string userMessage) + private static void ReportAssertAreNotEqualFailed(T notExpected, T actual, T delta, string userMessage) where T : struct, IConvertible { string finalMessage = string.Format( @@ -1323,7 +1323,7 @@ public static void AreEqual(string? expected, string? actual, bool ignoreCase, C } string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ThrowAssertAreEqualFailed(expected, actual, ignoreCase, culture, userMessage); + ReportAssertAreEqualFailed(expected, actual, ignoreCase, culture, userMessage); } /// @@ -1419,7 +1419,7 @@ public static void AreNotEqual(string? notExpected, string? actual, bool ignoreC } string userMessage = BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression); - ThrowAssertAreNotEqualFailed(notExpected, actual, userMessage); + ReportAssertAreNotEqualFailed(notExpected, actual, userMessage); } private static bool AreNotEqualFailing(string? notExpected, string? actual, bool ignoreCase, CultureInfo culture) @@ -1429,7 +1429,7 @@ private static bool AreNotEqualFailing(T? notExpected, T? actual, IEqualityCo => (comparer ?? EqualityComparer.Default).Equals(notExpected!, actual!); [DoesNotReturn] - private static void ThrowAssertAreNotEqualFailed(object? notExpected, object? actual, string userMessage) + private static void ReportAssertAreNotEqualFailed(object? notExpected, object? actual, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs index 723662c952..2960d31e69 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs @@ -39,7 +39,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "expected", expectedExpression, "actual", actualExpression) + " "); - ThrowAssertAreSameFailed(_expected, _actual, _builder.ToString()); + ReportAssertAreSameFailed(_expected, _actual, _builder.ToString()); } } @@ -95,7 +95,7 @@ internal void ComputeAssertion(string notExpectedExpression, string actualExpres if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "notExpected", notExpectedExpression, "actual", actualExpression) + " "); - ThrowAssertAreNotSameFailed(_builder.ToString()); + ReportAssertAreNotSameFailed(_builder.ToString()); } } @@ -178,14 +178,14 @@ public static void AreSame(T? expected, T? actual, string? message = "", [Cal } string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ThrowAssertAreSameFailed(expected, actual, userMessage); + ReportAssertAreSameFailed(expected, actual, userMessage); } private static bool IsAreSameFailing(T? expected, T? actual) => !object.ReferenceEquals(expected, actual); [DoesNotReturn] - private static void ThrowAssertAreSameFailed(T? expected, T? actual, string userMessage) + private static void ReportAssertAreSameFailed(T? expected, T? actual, string userMessage) { string finalMessage = userMessage; if (expected is ValueType && actual is ValueType) @@ -240,7 +240,7 @@ public static void AreNotSame(T? notExpected, T? actual, string? message = "" { if (IsAreNotSameFailing(notExpected, actual)) { - ThrowAssertAreNotSameFailed(BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression)); + ReportAssertAreNotSameFailed(BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression)); } } @@ -248,6 +248,6 @@ private static bool IsAreNotSameFailing(T? notExpected, T? actual) => object.ReferenceEquals(notExpected, actual); [DoesNotReturn] - private static void ThrowAssertAreNotSameFailed(string userMessage) + private static void ReportAssertAreNotSameFailed(string userMessage) => ReportAssertFailed("Assert.AreNotSame", userMessage); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs index 008eb40208..cb7b074f8c 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -40,7 +40,7 @@ internal TItem ComputeAssertion(string collectionExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "collection", collectionExpression) + " "); - ThrowAssertContainsSingleFailed(_actualCount, _builder.ToString()); + ReportAssertContainsSingleFailed(_actualCount, _builder.ToString()); } return _item!; @@ -165,12 +165,12 @@ public static T ContainsSingle(Func predicate, IEnumerable collec if (string.IsNullOrEmpty(predicateExpression)) { string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); - ThrowAssertContainsSingleFailed(actualCount, userMessage); + ReportAssertContainsSingleFailed(actualCount, userMessage); } else { string userMessage = BuildUserMessageForPredicateExpressionAndCollectionExpression(message, predicateExpression, collectionExpression); - ThrowAssertSingleMatchFailed(actualCount, userMessage); + ReportAssertSingleMatchFailed(actualCount, userMessage); } // Unreachable code but compiler cannot work it out @@ -226,12 +226,12 @@ public static T ContainsSingle(Func predicate, IEnumerable collec if (string.IsNullOrEmpty(predicateExpression)) { string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); - ThrowAssertContainsSingleFailed(matchCount, userMessage); + ReportAssertContainsSingleFailed(matchCount, userMessage); } else { string userMessage = BuildUserMessageForPredicateExpressionAndCollectionExpression(message, predicateExpression, collectionExpression); - ThrowAssertSingleMatchFailed(matchCount, userMessage); + ReportAssertSingleMatchFailed(matchCount, userMessage); } return default; @@ -261,7 +261,7 @@ public static void Contains(T expected, IEnumerable collection, string? me if (!collection.Contains(expected)) { string userMessage = BuildUserMessageForExpectedExpressionAndCollectionExpression(message, expectedExpression, collectionExpression); - ThrowAssertContainsItemFailed(userMessage); + ReportAssertContainsItemFailed(userMessage); } } @@ -292,7 +292,7 @@ public static void Contains(object? expected, IEnumerable collection, string? me } string userMessage = BuildUserMessageForExpectedExpressionAndCollectionExpression(message, expectedExpression, collectionExpression); - ThrowAssertContainsItemFailed(userMessage); + ReportAssertContainsItemFailed(userMessage); } /// @@ -316,7 +316,7 @@ public static void Contains(T expected, IEnumerable collection, IEqualityC if (!collection.Contains(expected, comparer)) { string userMessage = BuildUserMessageForExpectedExpressionAndCollectionExpression(message, expectedExpression, collectionExpression); - ThrowAssertContainsItemFailed(userMessage); + ReportAssertContainsItemFailed(userMessage); } } @@ -349,7 +349,7 @@ public static void Contains(object? expected, IEnumerable collection, IEqualityC } string userMessage = BuildUserMessageForExpectedExpressionAndCollectionExpression(message, expectedExpression, collectionExpression); - ThrowAssertContainsItemFailed(userMessage); + ReportAssertContainsItemFailed(userMessage); } /// @@ -372,7 +372,7 @@ public static void Contains(Func predicate, IEnumerable collectio if (!collection.Any(predicate)) { string userMessage = BuildUserMessageForPredicateExpressionAndCollectionExpression(message, predicateExpression, collectionExpression); - ThrowAssertContainsPredicateFailed(userMessage); + ReportAssertContainsPredicateFailed(userMessage); } } @@ -404,7 +404,7 @@ public static void Contains(Func predicate, IEnumerable collectio } string userMessage = BuildUserMessageForPredicateExpressionAndCollectionExpression(message, predicateExpression, collectionExpression); - ThrowAssertContainsPredicateFailed(userMessage); + ReportAssertContainsPredicateFailed(userMessage); } /// @@ -506,7 +506,7 @@ public static void DoesNotContain(T notExpected, IEnumerable collection, s if (collection.Contains(notExpected)) { string userMessage = BuildUserMessageForNotExpectedExpressionAndCollectionExpression(message, notExpectedExpression, collectionExpression); - ThrowAssertDoesNotContainItemFailed(userMessage); + ReportAssertDoesNotContainItemFailed(userMessage); } } @@ -533,7 +533,7 @@ public static void DoesNotContain(object? notExpected, IEnumerable collection, s if (object.Equals(notExpected, item)) { string userMessage = BuildUserMessageForNotExpectedExpressionAndCollectionExpression(message, notExpectedExpression, collectionExpression); - ThrowAssertDoesNotContainItemFailed(userMessage); + ReportAssertDoesNotContainItemFailed(userMessage); } } } @@ -559,7 +559,7 @@ public static void DoesNotContain(T notExpected, IEnumerable collection, I if (collection.Contains(notExpected, comparer)) { string userMessage = BuildUserMessageForNotExpectedExpressionAndCollectionExpression(message, notExpectedExpression, collectionExpression); - ThrowAssertDoesNotContainItemFailed(userMessage); + ReportAssertDoesNotContainItemFailed(userMessage); } } @@ -588,7 +588,7 @@ public static void DoesNotContain(object? notExpected, IEnumerable collection, I if (comparer.Equals(item, notExpected)) { string userMessage = BuildUserMessageForNotExpectedExpressionAndCollectionExpression(message, notExpectedExpression, collectionExpression); - ThrowAssertDoesNotContainItemFailed(userMessage); + ReportAssertDoesNotContainItemFailed(userMessage); } } } @@ -613,7 +613,7 @@ public static void DoesNotContain(Func predicate, IEnumerable col if (collection.Any(predicate)) { string userMessage = BuildUserMessageForPredicateExpressionAndCollectionExpression(message, predicateExpression, collectionExpression); - ThrowAssertDoesNotContainPredicateFailed(userMessage); + ReportAssertDoesNotContainPredicateFailed(userMessage); } } @@ -641,7 +641,7 @@ public static void DoesNotContain(Func predicate, IEnumerable col if (predicate(item)) { string userMessage = BuildUserMessageForPredicateExpressionAndCollectionExpression(message, predicateExpression, collectionExpression); - ThrowAssertDoesNotContainPredicateFailed(userMessage); + ReportAssertDoesNotContainPredicateFailed(userMessage); } } } @@ -765,7 +765,7 @@ public static void IsInRange(T minValue, T maxValue, T value, string? message #endregion // IsInRange [DoesNotReturn] - private static void ThrowAssertSingleMatchFailed(int actualCount, string userMessage) + private static void ReportAssertSingleMatchFailed(int actualCount, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -776,7 +776,7 @@ private static void ThrowAssertSingleMatchFailed(int actualCount, string userMes } [DoesNotReturn] - private static void ThrowAssertContainsSingleFailed(int actualCount, string userMessage) + private static void ReportAssertContainsSingleFailed(int actualCount, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -787,7 +787,7 @@ private static void ThrowAssertContainsSingleFailed(int actualCount, string user } [DoesNotReturn] - private static void ThrowAssertContainsItemFailed(string userMessage) + private static void ReportAssertContainsItemFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -797,7 +797,7 @@ private static void ThrowAssertContainsItemFailed(string userMessage) } [DoesNotReturn] - private static void ThrowAssertContainsPredicateFailed(string userMessage) + private static void ReportAssertContainsPredicateFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -807,7 +807,7 @@ private static void ThrowAssertContainsPredicateFailed(string userMessage) } [DoesNotReturn] - private static void ThrowAssertDoesNotContainItemFailed(string userMessage) + private static void ReportAssertDoesNotContainItemFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -817,7 +817,7 @@ private static void ThrowAssertDoesNotContainItemFailed(string userMessage) } [DoesNotReturn] - private static void ThrowAssertDoesNotContainPredicateFailed(string userMessage) + private static void ReportAssertDoesNotContainPredicateFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs index c8f64a9dc1..4ad00d8d02 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs @@ -48,7 +48,7 @@ internal void ComputeAssertion(string assertionName, string collectionExpression if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "collection", collectionExpression) + " "); - ThrowAssertCountFailed(assertionName, _expectedCount, _actualCount, _builder.ToString()); + ReportAssertCountFailed(assertionName, _expectedCount, _actualCount, _builder.ToString()); } } @@ -116,7 +116,7 @@ internal void ComputeAssertion(string collectionExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "collection", collectionExpression) + " "); - ThrowAssertIsNotEmptyFailed(_builder.ToString()); + ReportAssertIsNotEmptyFailed(_builder.ToString()); } } @@ -203,7 +203,7 @@ public static void IsNotEmpty(IEnumerable collection, string? message = "" } string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); - ThrowAssertIsNotEmptyFailed(userMessage); + ReportAssertIsNotEmptyFailed(userMessage); } /// @@ -223,7 +223,7 @@ public static void IsNotEmpty(IEnumerable collection, string? message = "", [Cal } string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); - ThrowAssertIsNotEmptyFailed(userMessage); + ReportAssertIsNotEmptyFailed(userMessage); } #endregion // IsNotEmpty @@ -327,14 +327,14 @@ private static void HasCount(string assertionName, int expected, IEnumerable< } string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); - ThrowAssertCountFailed(assertionName, expected, actualCount, userMessage); + ReportAssertCountFailed(assertionName, expected, actualCount, userMessage); } private static void HasCount(string assertionName, int expected, IEnumerable collection, string? message, string collectionExpression) => HasCount(assertionName, expected, collection.Cast(), message, collectionExpression); [DoesNotReturn] - private static void ThrowAssertCountFailed(string assertionName, int expectedCount, int actualCount, string userMessage) + private static void ReportAssertCountFailed(string assertionName, int expectedCount, int actualCount, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -346,7 +346,7 @@ private static void ThrowAssertCountFailed(string assertionName, int expectedCou } [DoesNotReturn] - private static void ThrowAssertIsNotEmptyFailed(string userMessage) + private static void ReportAssertIsNotEmptyFailed(string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs b/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs index 041c10ba45..cbe187dc13 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Fail.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,5 +22,5 @@ public sealed partial class Assert /// [DoesNotReturn] public static void Fail(string message = "") - => ReportAssertFailed("Assert.Fail", BuildUserMessage(message), forceThrow: true); + => ThrowAssertFailed("Assert.Fail", BuildUserMessage(message)); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs b/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs index cfc8240ad6..ba7f415a54 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IComparable.cs @@ -50,7 +50,7 @@ public static void IsGreaterThan(T lowerBound, T value, string? message = "", } string userMessage = BuildUserMessageForLowerBoundExpressionAndValueExpression(message, lowerBoundExpression, valueExpression); - ThrowAssertIsGreaterThanFailed(lowerBound, value, userMessage); + ReportAssertIsGreaterThanFailed(lowerBound, value, userMessage); } #endregion // IsGreaterThan @@ -95,7 +95,7 @@ public static void IsGreaterThanOrEqualTo(T lowerBound, T value, string? mess } string userMessage = BuildUserMessageForLowerBoundExpressionAndValueExpression(message, lowerBoundExpression, valueExpression); - ThrowAssertIsGreaterThanOrEqualToFailed(lowerBound, value, userMessage); + ReportAssertIsGreaterThanOrEqualToFailed(lowerBound, value, userMessage); } #endregion // IsGreaterThanOrEqualTo @@ -140,7 +140,7 @@ public static void IsLessThan(T upperBound, T value, string? message = "", [C } string userMessage = BuildUserMessageForUpperBoundExpressionAndValueExpression(message, upperBoundExpression, valueExpression); - ThrowAssertIsLessThanFailed(upperBound, value, userMessage); + ReportAssertIsLessThanFailed(upperBound, value, userMessage); } #endregion // IsLessThan @@ -185,7 +185,7 @@ public static void IsLessThanOrEqualTo(T upperBound, T value, string? message } string userMessage = BuildUserMessageForUpperBoundExpressionAndValueExpression(message, upperBoundExpression, valueExpression); - ThrowAssertIsLessThanOrEqualToFailed(upperBound, value, userMessage); + ReportAssertIsLessThanOrEqualToFailed(upperBound, value, userMessage); } #endregion // IsLessThanOrEqualTo @@ -222,14 +222,14 @@ public static void IsPositive(T value, string? message = "", [CallerArgumentE if (value is float.NaN) { string userMessage = BuildUserMessageForValueExpression(message, valueExpression); - ThrowAssertIsPositiveFailed(value, userMessage); + ReportAssertIsPositiveFailed(value, userMessage); return; } if (value is double.NaN) { string userMessage = BuildUserMessageForValueExpression(message, valueExpression); - ThrowAssertIsPositiveFailed(value, userMessage); + ReportAssertIsPositiveFailed(value, userMessage); return; } @@ -239,7 +239,7 @@ public static void IsPositive(T value, string? message = "", [CallerArgumentE } string userMessage2 = BuildUserMessageForValueExpression(message, valueExpression); - ThrowAssertIsPositiveFailed(value, userMessage2); + ReportAssertIsPositiveFailed(value, userMessage2); } #endregion // IsPositive @@ -276,14 +276,14 @@ public static void IsNegative(T value, string? message = "", [CallerArgumentE if (value is float.NaN) { string userMessage = BuildUserMessageForValueExpression(message, valueExpression); - ThrowAssertIsNegativeFailed(value, userMessage); + ReportAssertIsNegativeFailed(value, userMessage); return; } if (value is double.NaN) { string userMessage = BuildUserMessageForValueExpression(message, valueExpression); - ThrowAssertIsNegativeFailed(value, userMessage); + ReportAssertIsNegativeFailed(value, userMessage); return; } @@ -293,13 +293,13 @@ public static void IsNegative(T value, string? message = "", [CallerArgumentE } string userMessage2 = BuildUserMessageForValueExpression(message, valueExpression); - ThrowAssertIsNegativeFailed(value, userMessage2); + ReportAssertIsNegativeFailed(value, userMessage2); } #endregion // IsNegative [DoesNotReturn] - private static void ThrowAssertIsGreaterThanFailed(T lowerBound, T value, string userMessage) + private static void ReportAssertIsGreaterThanFailed(T lowerBound, T value, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -311,7 +311,7 @@ private static void ThrowAssertIsGreaterThanFailed(T lowerBound, T value, str } [DoesNotReturn] - private static void ThrowAssertIsGreaterThanOrEqualToFailed(T lowerBound, T value, string userMessage) + private static void ReportAssertIsGreaterThanOrEqualToFailed(T lowerBound, T value, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -323,7 +323,7 @@ private static void ThrowAssertIsGreaterThanOrEqualToFailed(T lowerBound, T v } [DoesNotReturn] - private static void ThrowAssertIsLessThanFailed(T upperBound, T value, string userMessage) + private static void ReportAssertIsLessThanFailed(T upperBound, T value, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -335,7 +335,7 @@ private static void ThrowAssertIsLessThanFailed(T upperBound, T value, string } [DoesNotReturn] - private static void ThrowAssertIsLessThanOrEqualToFailed(T upperBound, T value, string userMessage) + private static void ReportAssertIsLessThanOrEqualToFailed(T upperBound, T value, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -347,7 +347,7 @@ private static void ThrowAssertIsLessThanOrEqualToFailed(T upperBound, T valu } [DoesNotReturn] - private static void ThrowAssertIsPositiveFailed(T value, string userMessage) + private static void ReportAssertIsPositiveFailed(T value, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, @@ -358,7 +358,7 @@ private static void ThrowAssertIsPositiveFailed(T value, string userMessage) } [DoesNotReturn] - private static void ThrowAssertIsNegativeFailed(T value, string userMessage) + private static void ReportAssertIsNegativeFailed(T value, string userMessage) { string finalMessage = string.Format( CultureInfo.CurrentCulture, diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs b/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs index dd1af1f831..cca665dbb8 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index 02810ded5f..68cca35077 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -39,7 +39,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsExactInstanceOfTypeFailed(_value, _expectedType, _builder.ToString()); + ReportAssertIsExactInstanceOfTypeFailed(_value, _expectedType, _builder.ToString()); } } @@ -99,7 +99,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsExactInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); + ReportAssertIsExactInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); } } @@ -161,7 +161,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsNotExactInstanceOfTypeFailed(_value, _wrongType, _builder.ToString()); + ReportAssertIsNotExactInstanceOfTypeFailed(_value, _wrongType, _builder.ToString()); } } @@ -221,7 +221,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsNotExactInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); + ReportAssertIsNotExactInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); } } @@ -291,7 +291,7 @@ public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type { if (IsExactInstanceOfTypeFailing(value, expectedType)) { - ThrowAssertIsExactInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsExactInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); } } @@ -329,7 +329,7 @@ private static bool IsExactInstanceOfTypeFailing([NotNullWhen(false)] object? va => expectedType is null || value is null || value.GetType() != expectedType; [DoesNotReturn] - private static void ThrowAssertIsExactInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) + private static void ReportAssertIsExactInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) { string finalMessage = userMessage; if (expectedType is not null && value is not null) @@ -373,7 +373,7 @@ public static void IsNotExactInstanceOfType(object? value, [NotNull] Type? wrong { if (IsNotExactInstanceOfTypeFailing(value, wrongType)) { - ThrowAssertIsNotExactInstanceOfTypeFailed(value, wrongType, BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsNotExactInstanceOfTypeFailed(value, wrongType, BuildUserMessageForValueExpression(message, valueExpression)); } } @@ -405,7 +405,7 @@ private static bool IsNotExactInstanceOfTypeFailing(object? value, [NotNullWhen( (value is not null && value.GetType() == wrongType); [DoesNotReturn] - private static void ThrowAssertIsNotExactInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) + private static void ReportAssertIsNotExactInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) { string finalMessage = userMessage; if (wrongType is not null) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index 4c46c269a5..553a808132 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -39,7 +39,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsInstanceOfTypeFailed(_value, _expectedType, _builder.ToString()); + ReportAssertIsInstanceOfTypeFailed(_value, _expectedType, _builder.ToString()); } } @@ -99,7 +99,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); + ReportAssertIsInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); } } @@ -161,7 +161,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsNotInstanceOfTypeFailed(_value, _wrongType, _builder.ToString()); + ReportAssertIsNotInstanceOfTypeFailed(_value, _wrongType, _builder.ToString()); } } @@ -221,7 +221,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsNotInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); + ReportAssertIsNotInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); } } @@ -292,7 +292,7 @@ public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? exp { if (IsInstanceOfTypeFailing(value, expectedType)) { - ThrowAssertIsInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); } } @@ -331,7 +331,7 @@ private static bool IsInstanceOfTypeFailing([NotNullWhen(false)] object? value, => expectedType == null || value == null || !expectedType.IsInstanceOfType(value); [DoesNotReturn] - private static void ThrowAssertIsInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) + private static void ReportAssertIsInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) { string finalMessage = userMessage; if (expectedType is not null && value is not null) @@ -376,7 +376,7 @@ public static void IsNotInstanceOfType(object? value, [NotNull] Type? wrongType, { if (IsNotInstanceOfTypeFailing(value, wrongType)) { - ThrowAssertIsNotInstanceOfTypeFailed(value, wrongType, BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsNotInstanceOfTypeFailed(value, wrongType, BuildUserMessageForValueExpression(message, valueExpression)); } } @@ -409,7 +409,7 @@ private static bool IsNotInstanceOfTypeFailing(object? value, [NotNullWhen(false (value is not null && wrongType.IsInstanceOfType(value)); [DoesNotReturn] - private static void ThrowAssertIsNotInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) + private static void ReportAssertIsNotInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) { string finalMessage = userMessage; if (wrongType is not null) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index e8be59e897..00b3e1eec6 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -37,7 +37,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsNullFailed(_builder.ToString()); + ReportAssertIsNullFailed(_builder.ToString()); } } @@ -91,7 +91,7 @@ internal void ComputeAssertion(string valueExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ThrowAssertIsNotNullFailed(_builder.ToString()); + ReportAssertIsNotNullFailed(_builder.ToString()); } } @@ -152,13 +152,13 @@ public static void IsNull(object? value, string? message = "", [CallerArgumentEx { if (IsNullFailing(value)) { - ThrowAssertIsNullFailed(BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsNullFailed(BuildUserMessageForValueExpression(message, valueExpression)); } } private static bool IsNullFailing(object? value) => value is not null; - private static void ThrowAssertIsNullFailed(string? message) + private static void ReportAssertIsNullFailed(string? message) => ReportAssertFailed("Assert.IsNull", message); /// @@ -191,13 +191,13 @@ public static void IsNotNull([NotNull] object? value, string? message = "", [Cal { if (IsNotNullFailing(value)) { - ThrowAssertIsNotNullFailed(BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsNotNullFailed(BuildUserMessageForValueExpression(message, valueExpression)); } } private static bool IsNotNullFailing([NotNullWhen(false)] object? value) => value is null; [DoesNotReturn] - private static void ThrowAssertIsNotNullFailed(string? message) + private static void ReportAssertIsNotNullFailed(string? message) => ReportAssertFailed("Assert.IsNotNull", message); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs index 38e22cc71f..d1480fe701 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs @@ -37,7 +37,7 @@ internal void ComputeAssertion(string conditionExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "condition", conditionExpression) + " "); - ThrowAssertIsTrueFailed(_builder.ToString()); + ReportAssertIsTrueFailed(_builder.ToString()); } } @@ -89,7 +89,7 @@ internal void ComputeAssertion(string conditionExpression) if (_builder is not null) { _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "condition", conditionExpression) + " "); - ThrowAssertIsFalseFailed(_builder.ToString()); + ReportAssertIsFalseFailed(_builder.ToString()); } } @@ -150,14 +150,14 @@ public static void IsTrue([DoesNotReturnIf(false)] bool? condition, string? mess { if (IsTrueFailing(condition)) { - ThrowAssertIsTrueFailed(BuildUserMessageForConditionExpression(message, conditionExpression)); + ReportAssertIsTrueFailed(BuildUserMessageForConditionExpression(message, conditionExpression)); } } private static bool IsTrueFailing(bool? condition) => condition is false or null; - private static void ThrowAssertIsTrueFailed(string? message) + private static void ReportAssertIsTrueFailed(string? message) => ReportAssertFailed("Assert.IsTrue", message); /// @@ -188,7 +188,7 @@ public static void IsFalse([DoesNotReturnIf(true)] bool? condition, string? mess { if (IsFalseFailing(condition)) { - ThrowAssertIsFalseFailed(BuildUserMessageForConditionExpression(message, conditionExpression)); + ReportAssertIsFalseFailed(BuildUserMessageForConditionExpression(message, conditionExpression)); } } @@ -196,6 +196,6 @@ private static bool IsFalseFailing(bool? condition) => condition is true or null; [DoesNotReturn] - private static void ThrowAssertIsFalseFailed(string userMessage) + private static void ReportAssertIsFalseFailed(string userMessage) => ReportAssertFailed("Assert.IsFalse", userMessage); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.That.cs b/src/TestFramework/TestFramework/Assertions/Assert.That.cs index 6cb03ba5bb..03f85c2bb5 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.That.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.That.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Linq.Expressions; @@ -39,7 +39,6 @@ public static void That(Expression> condition, string? message = null var sb = new StringBuilder(); string expressionText = conditionExpression ?? throw new ArgumentNullException(nameof(conditionExpression)); - sb.AppendLine(string.Format(CultureInfo.InvariantCulture, FrameworkMessages.AssertThatFailedFormat, expressionText)); if (!string.IsNullOrWhiteSpace(message)) { sb.AppendLine(string.Format(CultureInfo.InvariantCulture, FrameworkMessages.AssertThatMessageFormat, message)); @@ -52,7 +51,7 @@ public static void That(Expression> condition, string? message = null sb.AppendLine(details); } - throw new AssertFailedException(sb.ToString().TrimEnd()); + Assert.ReportAssertFailed($"Assert.That({expressionText})", sb.ToString().TrimEnd()); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index de2886c107..af930e0fc0 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -25,6 +25,23 @@ private Assert() /// public static Assert That { get; } = new(); + /// + /// Reports an assertion failure and always throws, even within an . + /// + /// + /// name of the assertion throwing an exception. + /// + /// + /// The assertion failure message. + /// + [DoesNotReturn] + [StackTraceHidden] + internal static void ThrowAssertFailed(string assertionName, string? message) + { + LaunchDebuggerIfNeeded(); + throw CreateAssertFailedException(assertionName, message); + } + /// /// Reports an assertion failure. Within an , the failure is collected /// and execution continues. Outside a scope, the failure is thrown immediately. @@ -35,35 +52,24 @@ private Assert() /// /// The assertion failure message. /// - /// - /// When , the exception is always thrown, even within an . - /// #pragma warning disable CS8763 // A method marked [DoesNotReturn] should not return - Deliberately keeping [DoesNotReturn] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode). [DoesNotReturn] [StackTraceHidden] - internal static void ReportAssertFailed(string assertionName, string? message, bool forceThrow = false) + internal static void ReportAssertFailed(string assertionName, string? message) { - var assertionFailedException = new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); - if (!forceThrow && AssertScope.Current is { } scope) + LaunchDebuggerIfNeeded(); + AssertFailedException assertionFailedException = CreateAssertFailedException(assertionName, message); + if (AssertScope.Current is { } scope) { scope.AddError(assertionFailedException); return; } - ThrowAssertFailed(assertionFailedException); + throw assertionFailedException; } #pragma warning restore CS8763 // A method marked [DoesNotReturn] should not return - [DoesNotReturn] - [StackTraceHidden] - internal static void ThrowAssertFailed(AssertFailedException exception) - { - LaunchDebuggerIfNeeded(); - throw exception; - } - - [StackTraceHidden] - internal static void LaunchDebuggerIfNeeded() + private static void LaunchDebuggerIfNeeded() { if (ShouldLaunchDebugger()) { @@ -76,15 +82,19 @@ internal static void LaunchDebuggerIfNeeded() Debugger.Launch(); } } + + // Local functions + static bool ShouldLaunchDebugger() + => AssertionFailureSettings.LaunchDebuggerOnAssertionFailure switch + { + DebuggerLaunchMode.Enabled => true, + DebuggerLaunchMode.EnabledExcludingCI => !CIEnvironmentDetector.Instance.IsCIEnvironment(), + _ => false, + }; } - private static bool ShouldLaunchDebugger() - => AssertionFailureSettings.LaunchDebuggerOnAssertionFailure switch - { - DebuggerLaunchMode.Enabled => true, - DebuggerLaunchMode.EnabledExcludingCI => !CIEnvironmentDetector.Instance.IsCIEnvironment(), - _ => false, - }; + private static AssertFailedException CreateAssertFailedException(string assertionName, string? message) + => new(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); /// /// Builds the formatted message using the given user format message and parameters. diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs index b36bc7962f..c675c0d0e7 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertScope.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -56,16 +56,19 @@ public void Dispose() _disposed = true; CurrentScope.Value = null; + // We throw the collected exceptions directly instead of going through assertion failure + // helpers (e.g. ThrowAssertFailed) because the debugger was already launched when each + // error was collected. if (_errors.Count == 1 && _errors.TryDequeue(out AssertFailedException? singleError)) { - Assert.ThrowAssertFailed(singleError); + throw singleError; } if (!_errors.IsEmpty) { - Assert.ThrowAssertFailed(new AssertFailedException( + throw new AssertFailedException( string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), - new AggregateException(_errors))); + new AggregateException(_errors)); } } } diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 995b5b836a..f1df1de5f5 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -368,10 +368,6 @@ Actual: {2} '{0}' expression: '{1}'. Example: "'value' expression: 'new object()'", where 'value' is the parameter name of an assertion method, and 'new object()' is the expression the user passed to the assert method. - - Assert.That({0}) failed. - {0} is the user code expression - Message: {0} {0} user provided message diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index d6c8988178..f76d5972f2 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -88,11 +88,6 @@ Podrobnosti: - - Assert.That({0}) failed. - Assert.That({0}) se nezdařilo. - {0} is the user code expression - Message: {0} Zpráva: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index 06c1678819..1e469a372f 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -88,11 +88,6 @@ Details: - - Assert.That({0}) failed. - Assert.That({0}) fehlgeschlagen. - {0} is the user code expression - Message: {0} Meldung: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index e83482bce6..1c93644b1d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -88,11 +88,6 @@ Detalles: - - Assert.That({0}) failed. - Error de Assert.That({0}). - {0} is the user code expression - Message: {0} Mensaje: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index b215f7b50e..191b548e5c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -88,11 +88,6 @@ Détails : - - Assert.That({0}) failed. - Désolé, échec de Assert.That({0}). - {0} is the user code expression - Message: {0} Message : {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 420b48118d..0d7224dc21 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -88,11 +88,6 @@ Dettagli: - - Assert.That({0}) failed. - Assert.That({0}) non riuscito. - {0} is the user code expression - Message: {0} Messaggio: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index c6bd67b833..546f82aa58 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -88,11 +88,6 @@ 詳細: - - Assert.That({0}) failed. - Assert.That({0}) に失敗しました。 - {0} is the user code expression - Message: {0} メッセージ: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index 761092be62..149c306090 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -88,11 +88,6 @@ 세부 정보: - - Assert.That({0}) failed. - Assert.That({0})이(가) 실패했습니다. - {0} is the user code expression - Message: {0} 메시지: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 5698f2ff34..0b642c422b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -88,11 +88,6 @@ Szczegóły: - - Assert.That({0}) failed. - Operacja Assert.That({0}) nie powiodła się. - {0} is the user code expression - Message: {0} Komunikat: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index b509b4cabf..ecb9b6f66c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -88,11 +88,6 @@ Detalhes: - - Assert.That({0}) failed. - Assert.That({0}) falhou. - {0} is the user code expression - Message: {0} Mensagem: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 682759bdd7..85e71c9224 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -88,11 +88,6 @@ Подробности: - - Assert.That({0}) failed. - Сбой Assert.That({0}). - {0} is the user code expression - Message: {0} Сообщение: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 1622a38428..32f4cb0cc8 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -88,11 +88,6 @@ Ayrıntılar: - - Assert.That({0}) failed. - Assert.That({0}) başarısız oldu. - {0} is the user code expression - Message: {0} İleti: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 7f0070d43c..e3ef540d6a 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -88,11 +88,6 @@ 详细信息: - - Assert.That({0}) failed. - Assert.That({0}) 失败。 - {0} is the user code expression - Message: {0} 消息: {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 83cb88277f..c824e1d2be 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -88,11 +88,6 @@ 詳細資料: - - Assert.That({0}) failed. - Assert.That({0}) 失敗。 - {0} is the user code expression - Message: {0} 訊息: {0} From e84eadc14f292044d09bcd24ae349e3e68a90273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 16 Feb 2026 22:33:14 +0100 Subject: [PATCH 13/17] Final refactoring --- .../Assertions/Assert.Inconclusive.cs | 2 +- .../TestFramework/Assertions/Assert.That.cs | 6 ++++++ .../TestFramework/Assertions/Assert.cs | 14 ++++++++++++-- .../TestFramework/Resources/FrameworkMessages.resx | 2 +- .../Resources/xlf/FrameworkMessages.cs.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.de.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.es.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.fr.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.it.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.ja.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.ko.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.pl.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.ru.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.tr.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.zh-Hans.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.zh-Hant.xlf | 4 ++-- .../Assertions/AssertTests.That.cs | 8 ++++---- 18 files changed, 50 insertions(+), 34 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs b/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs index cca665dbb8..6923b71fbf 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Inconclusive.cs @@ -25,6 +25,6 @@ public static void Inconclusive(string message = "") { string userMessage = BuildUserMessage(message); throw new AssertInconclusiveException( - string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, "Assert.Inconclusive", userMessage)); + FormatAssertionFailed("Assert.Inconclusive", userMessage)); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.That.cs b/src/TestFramework/TestFramework/Assertions/Assert.That.cs index 03f85c2bb5..0399e6f27f 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.That.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.That.cs @@ -41,12 +41,18 @@ public static void That(Expression> condition, string? message = null ?? throw new ArgumentNullException(nameof(conditionExpression)); if (!string.IsNullOrWhiteSpace(message)) { + sb.AppendLine(); sb.AppendLine(string.Format(CultureInfo.InvariantCulture, FrameworkMessages.AssertThatMessageFormat, message)); } string details = ExtractDetails(condition.Body); if (!string.IsNullOrWhiteSpace(details)) { + if (sb.Length == 0) + { + sb.AppendLine(); + } + sb.AppendLine(FrameworkMessages.AssertThatDetailsPrefix); sb.AppendLine(details); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index af930e0fc0..62b72d6a14 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -94,7 +94,17 @@ static bool ShouldLaunchDebugger() } private static AssertFailedException CreateAssertFailedException(string assertionName, string? message) - => new(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message)); + => new(FormatAssertionFailed(assertionName, message)); + + private static string FormatAssertionFailed(string assertionName, string? message) + { + string failedMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName); + return string.IsNullOrWhiteSpace(message) + ? failedMessage + : message![0] is '\n' or '\r' + ? string.Concat(failedMessage, message) + : $"{failedMessage} {message}"; + } /// /// Builds the formatted message using the given user format message and parameters. @@ -221,7 +231,7 @@ internal static void CheckParameterNotNull([NotNull] object? param, string asser if (param is null) { string finalMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.NullParameterToAssert, parameterName); - throw new AssertFailedException(string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, finalMessage)); + throw CreateAssertFailedException(assertionName, finalMessage); } } diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index f1df1de5f5..261db6e409 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -201,7 +201,7 @@ Actual: {2} String '{0}' does not end with string '{1}'. {2} - {0} failed. {1} + {0} failed. {0} Expected type:<{1}>. Actual type:<{2}>. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index f76d5972f2..968e39e42b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -228,8 +228,8 @@ Skutečnost: {2} - {0} failed. {1} - {0} selhalo. {1} + {0} failed. + {0} selhalo. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index 1e469a372f..bd5cc60c92 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -228,8 +228,8 @@ Tatsächlich: {2} - {0} failed. {1} - Fehler bei "{0}". {1} + {0} failed. + Fehler bei "{0}". diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 1c93644b1d..11b959a8fc 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -228,8 +228,8 @@ Real: {2} - {0} failed. {1} - Error de {0}. {1} + {0} failed. + Error de {0}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 191b548e5c..85ed56c6e5 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -228,8 +228,8 @@ Réel : {2} - {0} failed. {1} - Échec de {0}. {1} + {0} failed. + Échec de {0}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 0d7224dc21..1056ec5868 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -228,8 +228,8 @@ Effettivo: {2} - {0} failed. {1} - {0} non riuscita. {1} + {0} failed. + {0} non riuscita. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index 546f82aa58..98cc5d2700 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -228,8 +228,8 @@ Actual: {2} - {0} failed. {1} - {0} に失敗しました。{1} + {0} failed. + {0} に失敗しました。 diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index 149c306090..e9b97a0951 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -228,8 +228,8 @@ Actual: {2} - {0} failed. {1} - {0}이(가) 실패했습니다. {1} + {0} failed. + {0}이(가) 실패했습니다. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 0b642c422b..5d0cdd5d04 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -228,8 +228,8 @@ Rzeczywiste: {2} - {0} failed. {1} - {0} — niepowodzenie. {1} + {0} failed. + {0} — niepowodzenie. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index ecb9b6f66c..9887ae3ee4 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -228,8 +228,8 @@ Real: {2} - {0} failed. {1} - {0} falhou. {1} + {0} failed. + {0} falhou. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 85e71c9224..1e94c49b50 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -228,8 +228,8 @@ Actual: {2} - {0} failed. {1} - Сбой {0}. {1} + {0} failed. + Сбой {0}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 32f4cb0cc8..7de6ae644b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -228,8 +228,8 @@ Gerçekte olan: {2} - {0} failed. {1} - {0} başarısız. {1} + {0} failed. + {0} başarısız. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index e3ef540d6a..d6079a5873 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -228,8 +228,8 @@ Actual: {2} - {0} failed. {1} - {0} 失败。{1} + {0} failed. + {0} 失败。 diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index c824e1d2be..681984062e 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -228,8 +228,8 @@ Actual: {2} - {0} failed. {1} - {0} 失敗。 {1} + {0} failed. + {0} 失敗。 diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.That.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.That.cs index 62297d11df..c090c24e4c 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.That.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.That.cs @@ -31,7 +31,7 @@ public void That_BooleanCondition_FailsAsExpected() act.Should().Throw() .WithMessage( """ - Assert.That(() => False) failed. + Assert.That(() => false) failed. Message: Boolean condition failed """); } @@ -763,7 +763,7 @@ public void That_WithBooleanLiterals_SkipsLiteralInDetails() act.Should().Throw() .WithMessage(""" - Assert.That(() => condition == True) failed. + Assert.That(() => condition == true) failed. Details: condition = False """); @@ -861,7 +861,7 @@ public void That_WithComplexConstantExpression_HandlesCorrectly() act.Should().Throw() .WithMessage(""" - Assert.That(() => dynamicValue.Length == ConstValue && flag == True) failed. + Assert.That(() => dynamicValue.Length == ConstValue && flag == true) failed. Details: dynamicValue.Length = 7 flag = False @@ -982,7 +982,7 @@ public void That_WithMixedLiteralsAndVariables_FiltersCorrectly() act.Should().Throw() .WithMessage(""" - Assert.That(() => name == "Admin" && age == 30 && isActive == False && grade == 'A') failed. + Assert.That(() => name == "Admin" && age == 30 && isActive == false && grade == 'A') failed. Details: age = 25 grade = B From b9d6aff189a379dc56a7b5fd58329512e084ded5 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Fri, 20 Feb 2026 11:58:16 +0100 Subject: [PATCH 14/17] Rely on UpdateXlf instead of manually modifying xlf --- .../Resources/xlf/FrameworkMessages.cs.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.de.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.es.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.fr.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.it.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.ja.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.ko.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.pl.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.ru.xlf | 24 +++++++++---------- .../Resources/xlf/FrameworkMessages.tr.xlf | 24 +++++++++---------- .../xlf/FrameworkMessages.zh-Hans.xlf | 24 +++++++++---------- .../xlf/FrameworkMessages.zh-Hant.xlf | 24 +++++++++---------- 13 files changed, 156 insertions(+), 156 deletions(-) diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index 968e39e42b..93ec92327d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -83,6 +83,16 @@ Nevkládejte hodnotu typů do AreSame(). Hodnoty převedené do typu Object už nebudou nikdy stejné. Zvažte použití AreEqual(). {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Podrobnosti: @@ -229,8 +239,8 @@ Skutečnost: {2} {0} failed. - {0} selhalo. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Skutečnost: {2} CollectionAssert.ReferenceEquals se nemá používat pro kontrolní výrazy. Místo toho použijte metody CollectionAssert nebo Assert.AreSame & overloads. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index bd5cc60c92..32fd458027 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -83,6 +83,16 @@ Übergeben Sie keine Werttypen an AreSame(). In ein Objekt konvertierte Werte sind niemals identisch. Verwenden Sie stattdessen AreEqual(). {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Details: @@ -229,8 +239,8 @@ Tatsächlich: {2} {0} failed. - Fehler bei "{0}". - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Tatsächlich: {2} CollectionAssert.ReferenceEquals darf nicht für Assertions verwendet werden. Verwenden Sie stattdessen CollectionAssert-Methoden oder Assert.AreSame und Überladungen. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 11b959a8fc..2353d57da4 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -83,6 +83,16 @@ No pase tipos de valor a AreSame(). Los valores convertidos a Object no serán nunca iguales. Considere el uso de AreEqual(). {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Detalles: @@ -229,8 +239,8 @@ Real: {2} {0} failed. - Error de {0}. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Real: {2} CollectionAssert.ReferenceEquals no se debe usar para las aserciones. Use los métodos CollectionAssert o las sobrecargas Assert.AreSame & en su lugar. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 85ed56c6e5..9f1e566fef 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -83,6 +83,16 @@ Ne passez pas de types valeur à AreSame(). Les valeurs converties en Object ne seront plus jamais les mêmes. Si possible, utilisez AreEqual(). {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Détails : @@ -229,8 +239,8 @@ Réel : {2} {0} failed. - Échec de {0}. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Réel : {2} CollectionAssert.ReferenceEquals ne doit pas être utilisé pour les assertions. Utilisez plutôt les méthodes CollectionAssert ou Assert.AreSame & overloads. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 1056ec5868..3b50a35bc0 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -83,6 +83,16 @@ Non passare tipi valore a AreSame(). I valori convertiti in Object non saranno mai uguali. Usare AreEqual(). {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Dettagli: @@ -229,8 +239,8 @@ Effettivo: {2} {0} failed. - {0} non riuscita. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Effettivo: {2} Non è possibile usare CollectionAssert.ReferenceEquals per le asserzioni. In alternativa, usare i metodi CollectionAssert o Assert.AreSame e gli overload. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index 98cc5d2700..bea427f7db 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -83,6 +83,16 @@ AreSame() には値型を渡すことはできません。オブジェクトに変換された値が同じにはなりません。AreEqual() を使用することを検討してください。{0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: 詳細: @@ -229,8 +239,8 @@ Actual: {2} {0} failed. - {0} に失敗しました。 - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Actual: {2} アサーションには CollectionAssert.ReferenceEquals を使用しないでください。代わりに CollectionAssert メソッドまたは Assert.AreSame およびオーバーロードを使用してください。 - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index e9b97a0951..80527611b9 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -83,6 +83,16 @@ AreSame()에 값 형식을 전달하면 안 됩니다. Object로 변환된 값은 동일한 값으로 간주되지 않습니다. AreEqual()을 사용해 보세요. {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: 세부 정보: @@ -229,8 +239,8 @@ Actual: {2} {0} failed. - {0}이(가) 실패했습니다. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Actual: {2} CollectionAssert.ReferenceEquals는 Assertions에 사용할 수 없습니다. 대신 CollectionAssert 메서드 또는 Assert.AreSame 및 오버로드를 사용하세요. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 5d0cdd5d04..331ae17a30 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -83,6 +83,16 @@ Nie przekazuj typów wartości do metody AreSame(). Wartości przekonwertowane na typ Object nigdy nie będą takie same. Rozważ użycie metody AreEqual(). {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Szczegóły: @@ -229,8 +239,8 @@ Rzeczywiste: {2} {0} failed. - {0} — niepowodzenie. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Rzeczywiste: {2} Element Assert.ReferenceEquals nie powinien być używany dla asercji. Zamiast tego użyj metod CollectionAssert lub Assert.AreSame oraz ich przeciążeń. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index 9887ae3ee4..202af7d4fb 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -83,6 +83,16 @@ Não passe tipos de valores para AreSame(). Os valores convertidos para Object nunca serão os mesmos. Considere usar AreEqual(). {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Detalhes: @@ -229,8 +239,8 @@ Real: {2} {0} failed. - {0} falhou. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Real: {2} CollectionAssert.ReferenceEquals não deve ser usado com as Declarações. Em vez disso, use os métodos CollectionAssert ou Assert.AreSame e as sobrecargas. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 1e94c49b50..489ca58a9c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -83,6 +83,16 @@ Не передавайте типы значений в функцию AreSame(). Значения, преобразованные в Object, никогда не будут одинаковыми. Попробуйте использовать AreEqual(). {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Подробности: @@ -229,8 +239,8 @@ Actual: {2} {0} failed. - Сбой {0}. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Actual: {2} Нельзя использовать CollectionAssert.ReferenceEquals для Assertions. Вместо этого используйте методы CollectionAssert или Assert.AreSame и перегрузки. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 7de6ae644b..d556b3df15 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -83,6 +83,16 @@ AreSame()'e değer türleri geçirmeyin. Object olarak dönüştürülen değerler asla aynı olamayacak. AreEqual() kullanmayı düşün. {0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: Ayrıntılar: @@ -229,8 +239,8 @@ Gerçekte olan: {2} {0} failed. - {0} başarısız. - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Gerçekte olan: {2} CollectionAssert.ReferenceEquals, Onaylama için kullanılmamalı. Lütfen bunun yerine CollectionAssert yöntemlerini veya Assert.AreSame & aşırı yüklemelerini kullanın. - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index d6079a5873..433dea62e4 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -83,6 +83,16 @@ 不要向 AreSame() 传递值类型。转换为 Object 的值将永远不会相等。请考虑使用 AreEqual()。{0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: 详细信息: @@ -229,8 +239,8 @@ Actual: {2} {0} failed. - {0} 失败。 - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Actual: {2} CollectionAssert.ReferenceEquals 不应用于断言。请改用 CollectionAssert 方法或 Assert.AreSame 和重载。 - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 681984062e..0ca92f80f1 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -83,6 +83,16 @@ 不要將實值型別傳遞給 AreSame()。轉換成 Object 的值從此不再一樣。請考慮使用 AreEqual()。{0} + + {0} assertion(s) failed within the assert scope. + {0} assertion(s) failed within the assert scope. + {0} is the number of assertion failures collected in the scope. + + + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + Nested assert scopes are not allowed. Dispose the current scope before creating a new one. + + Details: 詳細資料: @@ -229,8 +239,8 @@ Actual: {2} {0} failed. - {0} 失敗。 - + {0} failed. + Expected collection of size {1}. Actual: {2}. {0} @@ -453,16 +463,6 @@ Actual: {2} CollectionAssert.ReferenceEquals 不應使用於判斷提示。請改用 CollectionAssert 方法或 Assert.AreSame 及其多載。 - - {0} assertion(s) failed within the assert scope. - {0} assertion(s) failed within the assert scope. - {0} is the number of assertion failures collected in the scope. - - - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - Nested assert scopes are not allowed. Dispose the current scope before creating a new one. - - \ No newline at end of file From bc227a8c42cf67d3452bc6af48f4b5cd37d53834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 20 Feb 2026 14:38:56 +0100 Subject: [PATCH 15/17] Improve tests --- .../SoftAssertionTests.cs | 200 ++++++++++++++++++ .../Assertions/AssertTests.ScopeTests.cs | 10 +- 2 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs new file mode 100644 index 0000000000..6730d9bb15 --- /dev/null +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Acceptance.IntegrationTests; +using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers; +using Microsoft.Testing.Platform.Helpers; + +namespace MSTest.Acceptance.IntegrationTests; + +[TestClass] +public sealed class SoftAssertionTests : AcceptanceTestBase +{ + [TestMethod] + public async Task ScopeWithNoFailures_TestPasses() + { + var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithNoFailures", cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertOutputContains("passed ScopeWithNoFailures"); + } + + [TestMethod] + public async Task ScopeWithSingleFailure_TestFails() + { + var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithSingleFailure", cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertOutputContains("failed ScopeWithSingleFailure"); + testHostResult.AssertOutputContains("Assert.AreEqual failed. Expected:<1>. Actual:<2>."); + } + + [TestMethod] + public async Task ScopeWithMultipleFailures_TestFailsWithAggregatedMessage() + { + var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithMultipleFailures", cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertOutputContains("failed ScopeWithMultipleFailures"); + testHostResult.AssertOutputContains("2 assertion(s) failed within the assert scope."); + } + + [TestMethod] + public async Task AssertFailIsHardFailure_ThrowsImmediately() + { + var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter AssertFailIsHardFailure", cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertOutputContains("failed AssertFailIsHardFailure"); + testHostResult.AssertOutputContains("Assert.Fail failed. hard failure"); + // The second Assert.Fail should not be reached because Assert.Fail throws immediately. + testHostResult.AssertOutputDoesNotContain("second failure"); + } + + [TestMethod] + public async Task ScopeWithSoftFailureFollowedByException_CollectsBoth() + { + var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter SoftFailureFollowedByException", cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertOutputContains("failed SoftFailureFollowedByException"); + } + + [TestMethod] + public async Task ScopeWithIsNotNullSoftFailure_CollectsFailure() + { + var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithIsNotNullSoftFailure", cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); + testHostResult.AssertOutputContains("failed ScopeWithIsNotNullSoftFailure"); + testHostResult.AssertOutputContains("Assert.IsNotNull failed."); + } + + [TestMethod] + public async Task ScopeAssertionsAreIndependentBetweenTests_SecondTestPasses() + { + var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter IndependentTest", cancellationToken: TestContext.CancellationToken); + + testHostResult.AssertExitCodeIs(ExitCodes.Success); + testHostResult.AssertOutputContains("passed IndependentTest"); + } + + public sealed class TestAssetFixture() : TestAssetFixtureBase(AcceptanceFixture.NuGetGlobalPackagesFolder) + { + public const string ProjectName = "SoftAssertionTests"; + + public string ProjectPath => GetAssetPath(ProjectName); + + public override IEnumerable<(string ID, string Name, string Code)> GetAssetsToGenerate() + { + yield return (ProjectName, ProjectName, + SourceCode + .PatchTargetFrameworks(TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion)); + } + + private const string SourceCode = """ +#file SoftAssertionTests.csproj + + + + Exe + true + $TargetFrameworks$ + $(NoWarn);MSTESTEXP + + + + + + + + + +#file UnitTest1.cs +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void ScopeWithNoFailures() + { + using (Assert.Scope()) + { + Assert.IsTrue(true); + Assert.AreEqual(1, 1); + } + } + + [TestMethod] + public void ScopeWithSingleFailure() + { + using (Assert.Scope()) + { + Assert.AreEqual(1, 2); + } + } + + [TestMethod] + public void ScopeWithMultipleFailures() + { + using (Assert.Scope()) + { + Assert.AreEqual(1, 2); + Assert.IsTrue(false); + } + } + + [TestMethod] + public void AssertFailIsHardFailure() + { + using (Assert.Scope()) + { + Assert.Fail("hard failure"); + Assert.Fail("second failure"); + } + } + + [TestMethod] + public void SoftFailureFollowedByException() + { + string x = null; + using (Assert.Scope()) + { + Assert.IsNotNull(x); + Assert.AreEqual(1, x.Length); // throws NullReferenceException + } + } + + [TestMethod] + public void ScopeWithIsNotNullSoftFailure() + { + object value = null; + using (Assert.Scope()) + { + Assert.IsNotNull(value); + Assert.AreEqual(1, 1); + } + } + + [TestMethod] + public void IndependentTest() + { + // Verify that a scope from a previous test does not leak into this test. + Assert.IsTrue(true); + } +} +"""; + } + + public TestContext TestContext { get; set; } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index f99c7caea8..096dbb9905 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -23,13 +23,9 @@ public void Scope_NoFailures_DoesNotThrow() public void Scope_SingleFailure_ThrowsOnDispose() { - Action action = () => - { - using (Assert.Scope()) - { - Assert.AreEqual(1, 2); - } - }; + IDisposable scope = Assert.Scope(); + Assert.AreEqual(1, 2); + Action action = () => scope.Dispose(); action.Should().Throw() .WithMessage("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); From 13ccc3bc5301b20cc4649fd8f126eea030180168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 20 Feb 2026 14:43:16 +0100 Subject: [PATCH 16/17] Fix --- .../MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs index 6730d9bb15..87587cd82a 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs @@ -17,7 +17,7 @@ public async Task ScopeWithNoFailures_TestPasses() TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithNoFailures", cancellationToken: TestContext.CancellationToken); testHostResult.AssertExitCodeIs(ExitCodes.Success); - testHostResult.AssertOutputContains("passed ScopeWithNoFailures"); + testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); } [TestMethod] @@ -83,7 +83,7 @@ public async Task ScopeAssertionsAreIndependentBetweenTests_SecondTestPasses() TestHostResult testHostResult = await testHost.ExecuteAsync("--filter IndependentTest", cancellationToken: TestContext.CancellationToken); testHostResult.AssertExitCodeIs(ExitCodes.Success); - testHostResult.AssertOutputContains("passed IndependentTest"); + testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0); } public sealed class TestAssetFixture() : TestAssetFixtureBase(AcceptanceFixture.NuGetGlobalPackagesFolder) From 086a07264b9b20f54e6f25d883bbfa1e87aaf94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 20 Feb 2026 16:01:07 +0100 Subject: [PATCH 17/17] Address review comments --- .../011-Soft-Assertions-Nullability-Design.md | 2 +- .../TestFramework/Assertions/Assert.cs | 11 +++++++++ .../TestFramework/Assertions/AssertScope.cs | 20 +++++++++++----- .../SoftAssertionTests.cs | 24 +++++++++++-------- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/docs/RFCs/011-Soft-Assertions-Nullability-Design.md b/docs/RFCs/011-Soft-Assertions-Nullability-Design.md index 2696229e16..e16a4d81d3 100644 --- a/docs/RFCs/011-Soft-Assertions-Nullability-Design.md +++ b/docs/RFCs/011-Soft-Assertions-Nullability-Design.md @@ -27,7 +27,7 @@ using (Assert.Scope()) Soft assertions create a fundamental tension with C# nullability annotations and, more broadly, with all assertion postconditions. -Before soft assertions, `ReportAssertFailed` was annotated with `[DoesNotReturn]`, which let the compiler prove post-condition contracts. For example: +Before soft assertions, the helper method responsible for reporting assertion failures (for example, `ThrowAssertFailed`) was annotated with `[DoesNotReturn]`, which let the compiler prove post-condition contracts. For example: ```csharp public static void IsNotNull([NotNull] object? value, ...) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 62b72d6a14..0703444ae6 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -61,6 +61,17 @@ internal static void ReportAssertFailed(string assertionName, string? message) AssertFailedException assertionFailedException = CreateAssertFailedException(assertionName, message); if (AssertScope.Current is { } scope) { + // Throw and catch to capture the stack trace at the point of failure, + // so the exception has a meaningful stack trace when reported from the scope. + try + { + throw assertionFailedException; + } + catch (AssertFailedException ex) + { + assertionFailedException = ex; + } + scope.AddError(assertionFailedException); return; } diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs index c675c0d0e7..cef4dbd0a0 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertScope.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Runtime.ExceptionServices; + namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// @@ -35,7 +37,7 @@ internal AssertScope() /// The assertion failure message. internal void AddError(AssertFailedException error) { -#pragma warning disable CA1513 // Use ObjectDisposedException throw helper +#pragma warning disable CA1513 // Use ObjectDisposedException throw helper - ThrowIf is not available on all target frameworks if (_disposed) { throw new ObjectDisposedException(nameof(AssertScope)); @@ -59,16 +61,22 @@ public void Dispose() // We throw the collected exceptions directly instead of going through assertion failure // helpers (e.g. ThrowAssertFailed) because the debugger was already launched when each // error was collected. - if (_errors.Count == 1 && _errors.TryDequeue(out AssertFailedException? singleError)) + // Snapshot the ConcurrentQueue into an array to avoid multiple O(n) enumerations + // (ConcurrentQueue.Count is O(n)) and to get a consistent view for branching, + // message formatting, and building the AggregateException. + AssertFailedException[] errorsSnapshot = _errors.ToArray(); + if (errorsSnapshot.Length == 1) { - throw singleError; + // Use ExceptionDispatchInfo to preserve the original stack trace captured at the + // assertion call site, rather than resetting it to point at Dispose. + ExceptionDispatchInfo.Capture(errorsSnapshot[0]).Throw(); } - if (!_errors.IsEmpty) + if (errorsSnapshot.Length > 0) { throw new AssertFailedException( - string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, _errors.Count), - new AggregateException(_errors)); + string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertScopeFailure, errorsSnapshot.Length), + new AggregateException(errorsSnapshot)); } } } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs index 87587cd82a..594dc653d1 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs @@ -27,8 +27,8 @@ public async Task ScopeWithSingleFailure_TestFails() TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithSingleFailure", cancellationToken: TestContext.CancellationToken); testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); - testHostResult.AssertOutputContains("failed ScopeWithSingleFailure"); - testHostResult.AssertOutputContains("Assert.AreEqual failed. Expected:<1>. Actual:<2>."); + testHostResult.AssertOutputMatchesRegex( + """failed ScopeWithSingleFailure \(\d+ms\)[\s\S]+Assert\.AreEqual failed\. Expected:<1>\. Actual:<2>\.[\s\S]+at UnitTest1\.ScopeWithSingleFailure\(\)"""); } [TestMethod] @@ -38,8 +38,10 @@ public async Task ScopeWithMultipleFailures_TestFailsWithAggregatedMessage() TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithMultipleFailures", cancellationToken: TestContext.CancellationToken); testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); - testHostResult.AssertOutputContains("failed ScopeWithMultipleFailures"); - testHostResult.AssertOutputContains("2 assertion(s) failed within the assert scope."); + // Validate the output includes the aggregate message and that inner exception stack traces + // point to the test method (assertion call site). + testHostResult.AssertOutputMatchesRegex( + """failed ScopeWithMultipleFailures \(\d+ms\)[\s\S]+2 assertion\(s\) failed within the assert scope\.[\s\S]+Assert\.AreEqual failed\. Expected:<1>\. Actual:<2>\.[\s\S]+at UnitTest1\.ScopeWithMultipleFailures\(\)[\s\S]+Assert\.IsTrue failed\.[\s\S]+at UnitTest1\.ScopeWithMultipleFailures\(\)"""); } [TestMethod] @@ -49,9 +51,10 @@ public async Task AssertFailIsHardFailure_ThrowsImmediately() TestHostResult testHostResult = await testHost.ExecuteAsync("--filter AssertFailIsHardFailure", cancellationToken: TestContext.CancellationToken); testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); - testHostResult.AssertOutputContains("failed AssertFailIsHardFailure"); - testHostResult.AssertOutputContains("Assert.Fail failed. hard failure"); - // The second Assert.Fail should not be reached because Assert.Fail throws immediately. + // Assert.Fail is a hard assertion — it throws immediately, even within a scope. + // The second Assert.Fail should not be reached. + testHostResult.AssertOutputMatchesRegex( + """failed AssertFailIsHardFailure \(\d+ms\)[\s\S]+Assert\.Fail failed\. hard failure"""); testHostResult.AssertOutputDoesNotContain("second failure"); } @@ -62,7 +65,8 @@ public async Task ScopeWithSoftFailureFollowedByException_CollectsBoth() TestHostResult testHostResult = await testHost.ExecuteAsync("--filter SoftFailureFollowedByException", cancellationToken: TestContext.CancellationToken); testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); - testHostResult.AssertOutputContains("failed SoftFailureFollowedByException"); + testHostResult.AssertOutputMatchesRegex( + """failed SoftFailureFollowedByException \(\d+ms\)[\s\S]+at UnitTest1\.SoftFailureFollowedByException\(\)"""); } [TestMethod] @@ -72,8 +76,8 @@ public async Task ScopeWithIsNotNullSoftFailure_CollectsFailure() TestHostResult testHostResult = await testHost.ExecuteAsync("--filter ScopeWithIsNotNullSoftFailure", cancellationToken: TestContext.CancellationToken); testHostResult.AssertExitCodeIs(ExitCodes.AtLeastOneTestFailed); - testHostResult.AssertOutputContains("failed ScopeWithIsNotNullSoftFailure"); - testHostResult.AssertOutputContains("Assert.IsNotNull failed."); + testHostResult.AssertOutputMatchesRegex( + """failed ScopeWithIsNotNullSoftFailure \(\d+ms\)[\s\S]+Assert\.IsNotNull failed\.[\s\S]+at UnitTest1\.ScopeWithIsNotNullSoftFailure\(\)"""); } [TestMethod]