From ce65e45dd0ff5d8e127ff00459df437065bb12bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 20 May 2026 16:58:37 +0200 Subject: [PATCH 1/6] Apply [StackTraceHidden] to Assert/CollectionAssert/StringAssert (fixes #8277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Annotate every public assertion type — Assert, CollectionAssert, StringAssert, AssertExtensions (Assert.That) — and the internal helpers that sit on the assertion call path (AssertScope, StructuredAssertionMessage, EvidenceBlock, EvidenceLine, AssertionValueRenderer) plus the nested InterpolatedStringHandler structs with [StackTraceHidden]. On .NET 6+ the runtime now omits MSTest framework frames from Exception.StackTrace, so the user-visible call stack points at the test method instead of e.g. Assert.IComparable.cs. On netstandard2.0 / .NET Framework the attribute is a no-op (polyfilled internal type) and stacks remain unchanged. Adds AssertStackTraceHiddenTests verifying that no MSTest framework frame appears in the captured stack for representative assertion failures on .NET 6+, plus an end-to-end AzureDevOpsReporter test that asserts a real Assert.AreEqual failure annotates the test method's source file rather than a framework partial-class file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ert.AreEqual.InterpolatedStringHandlers.cs | 4 + .../Assertions/Assert.AreSame.cs | 2 + .../Assertions/Assert.Contains.cs | 1 + .../TestFramework/Assertions/Assert.Count.cs | 2 + .../Assert.IsExactInstanceOfType.cs | 4 + .../Assertions/Assert.IsInstanceOfType.cs | 4 + .../TestFramework/Assertions/Assert.IsNull.cs | 2 + .../TestFramework/Assertions/Assert.IsTrue.cs | 2 + .../TestFramework/Assertions/Assert.That.cs | 1 + .../Assertions/Assert.ThrowsException.cs | 2 + .../TestFramework/Assertions/Assert.cs | 1 + .../TestFramework/Assertions/AssertScope.cs | 1 + .../Assertions/AssertionValueRenderer.cs | 1 + .../Assertions/CollectionAssert.cs | 1 + .../TestFramework/Assertions/EvidenceBlock.cs | 1 + .../TestFramework/Assertions/EvidenceLine.cs | 1 + .../TestFramework/Assertions/StringAssert.cs | 1 + .../Assertions/StructuredAssertionMessage.cs | 1 + .../AzureDevOpsTests.cs | 60 ++++++++ .../Assertions/AssertStackTraceHiddenTests.cs | 136 ++++++++++++++++++ 20 files changed, 228 insertions(+) create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs index 888d647cc8..fabe9b7005 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs @@ -12,6 +12,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -86,6 +87,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertAreNotEqualInterpolatedStringHandler @@ -152,6 +154,7 @@ internal void ComputeAssertion(string notExpectedExpression, string actualExpres #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertNonGenericAreEqualInterpolatedStringHandler @@ -263,6 +266,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertNonGenericAreNotEqualInterpolatedStringHandler diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs index 2a3eaf72dc..189a317a02 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs @@ -14,6 +14,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -74,6 +75,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertAreNotSameInterpolatedStringHandler diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs index eb078f064f..ad6477c4e9 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Contains.cs @@ -12,6 +12,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs index 470f8d6383..560d7c41a7 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs @@ -12,6 +12,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -95,6 +96,7 @@ public void AppendFormatted(object? value, int alignment = 0, string? format = n #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertIsNotEmptyInterpolatedStringHandler diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index 5ac767cca9..f1d00d046f 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -14,6 +14,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -76,6 +77,7 @@ internal void ComputeAssertion(string valueExpression) #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertGenericIsExactInstanceOfTypeInterpolatedStringHandler @@ -135,6 +137,7 @@ internal void ComputeAssertion(string valueExpression) #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertIsNotExactInstanceOfTypeInterpolatedStringHandler @@ -196,6 +199,7 @@ internal void ComputeAssertion(string valueExpression) #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertGenericIsNotExactInstanceOfTypeInterpolatedStringHandler diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index 872c70da1f..3c6897e0be 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -14,6 +14,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -76,6 +77,7 @@ internal void ComputeAssertion(string valueExpression) #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertGenericIsInstanceOfTypeInterpolatedStringHandler @@ -135,6 +137,7 @@ internal void ComputeAssertion(string valueExpression) #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertIsNotInstanceOfTypeInterpolatedStringHandler @@ -196,6 +199,7 @@ internal void ComputeAssertion(string valueExpression) #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertGenericIsNotInstanceOfTypeInterpolatedStringHandler diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index 033347aa30..4a7a34073d 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -16,6 +16,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -71,6 +72,7 @@ internal void ComputeAssertion(string valueExpression) } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs index 88647fff5a..56e10d6dea 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs @@ -16,6 +16,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -70,6 +71,7 @@ internal void ComputeAssertion(string conditionExpression) public void AppendFormatted(object? value, int alignment = 0, string? format = null) => _builder!.AppendFormat(null, $"{{0,{alignment}:{format}}}", value); } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertIsFalseInterpolatedStringHandler diff --git a/src/TestFramework/TestFramework/Assertions/Assert.That.cs b/src/TestFramework/TestFramework/Assertions/Assert.That.cs index 61589839b6..b5e3b703da 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.That.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.That.cs @@ -8,6 +8,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// /// Provides That extension to Assert class. /// +[StackTraceHidden] public static partial class AssertExtensions { // Constants for standardized display values diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs index c82a078cad..b2b2c7df98 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.cs @@ -14,6 +14,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// public sealed partial class Assert { + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -85,6 +86,7 @@ internal TException ComputeAssertion(string actionExpression) #pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads } + [StackTraceHidden] [InterpolatedStringHandler] [EditorBrowsable(EditorBrowsableState.Never)] public readonly struct AssertThrowsExactlyInterpolatedStringHandler diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 2f4874cbe0..b345210c39 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -8,6 +8,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// unit tests. If the condition being tested is not met, an exception /// is thrown. /// +[StackTraceHidden] public sealed partial class Assert { private Assert() diff --git a/src/TestFramework/TestFramework/Assertions/AssertScope.cs b/src/TestFramework/TestFramework/Assertions/AssertScope.cs index 0d0b678ea0..e12f060cbb 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertScope.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertScope.cs @@ -9,6 +9,7 @@ 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 . /// +[StackTraceHidden] internal sealed class AssertScope : IDisposable { private static readonly AsyncLocal CurrentScope = new(); diff --git a/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs index fd34336624..b49b85823e 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs @@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// /// Renders values for display in structured assertion messages following the RFC 012 value rendering rules. /// +[StackTraceHidden] internal static class AssertionValueRenderer { /// diff --git a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs index 56dd75d81c..30a2558bb4 100644 --- a/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/CollectionAssert.cs @@ -8,6 +8,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// with collections within unit tests. If the condition being tested is not /// met, an exception is thrown. /// +[StackTraceHidden] public sealed partial class CollectionAssert { #region Singleton constructor diff --git a/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs b/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs index 25e9bfac22..8094e17bd6 100644 --- a/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs +++ b/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs @@ -7,6 +7,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// Represents the evidence block of a structured assertion message, containing labeled value lines /// such as expected/actual values and assertion-specific details. /// +[StackTraceHidden] internal sealed class EvidenceBlock { private readonly List _lines = []; diff --git a/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs b/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs index bb09f5f150..420482e77f 100644 --- a/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs +++ b/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs @@ -6,4 +6,5 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// /// Represents a single labeled line in the evidence block of a structured assertion message. /// +[StackTraceHidden] internal readonly record struct EvidenceLine(string Label, string Value); diff --git a/src/TestFramework/TestFramework/Assertions/StringAssert.cs b/src/TestFramework/TestFramework/Assertions/StringAssert.cs index f3bebb718f..08106d7e22 100644 --- a/src/TestFramework/TestFramework/Assertions/StringAssert.cs +++ b/src/TestFramework/TestFramework/Assertions/StringAssert.cs @@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// /// The string assert. /// +[StackTraceHidden] public sealed class StringAssert { #region Singleton constructor diff --git a/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs b/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs index 01b55290cf..523071de8d 100644 --- a/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs +++ b/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs @@ -14,6 +14,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// <call-site expression> /// /// +[StackTraceHidden] internal sealed class StructuredAssertionMessage { private const string AssertionPrefix = "Assertion failed."; diff --git a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsTests.cs index 7df65b6273..e912fa3249 100644 --- a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsTests.cs +++ b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsTests.cs @@ -198,6 +198,66 @@ public void DoesNotSkipUserFrameWhoseTypeNameStartsWithAssert() $"\nLogs:\n{string.Join("\n", logger.Logs)}"); } + [TestMethod] + public void RealAssertFailure_AnnotationPointsAtTestMethodAndNotAtFrameworkFile() + { + // End-to-end acceptance for https://github.com/microsoft/testfx/issues/8277. + // + // Trigger a *real* MSTest assertion failure (no synthetic stack trace) and ensure the + // AzDO reporter annotates the test method's source file rather than the framework + // partial-class file that actually holds the failing implementation. + // + // This regression is double-guarded: + // * On .NET 6+: every public Assert/CollectionAssert/StringAssert method carries + // [StackTraceHidden], so the framework frame should not even appear in the + // captured stack trace. + // * On older runtimes (.NET Framework / netstandard2.0 consumers): the attribute is + // a no-op, but the reporter's type-prefix heuristic still skips the framework + // frame. + // Either way, the annotation must point at this test method's source file. + (string testFile, _) = GetCurrentLocation(); + + AssertFailedException error; + try + { +#pragma warning disable MSTEST0025 // Use 'Assert.Fail' instead of an always-failing assert - we deliberately want a real Assert.AreEqual failure so the captured stack trace reflects what users see. + Assert.AreEqual(1, 2); +#pragma warning restore MSTEST0025 + throw new InvalidOperationException("Expected AssertFailedException was not thrown."); + } + catch (AssertFailedException ex) + { + error = ex; + } + + var logger = new TextLogger(); + string? text = AzureDevOpsReporter.GetErrorText( + "MyTestDisplayName", + explanation: null, + error, + "severity", + new SystemFileSystem(), + logger, + "net9.0"); + + Assert.IsNotNull(text, $"AzDO reporter should have produced an annotation. Logs:\n{string.Join("\n", logger.Logs)}"); + + string normalizedTestFile = testFile.Replace('\\', '/'); + string repoRoot = RootFinder.Find().Replace('\\', '/'); + string expectedRelativeFile = normalizedTestFile.StartsWith(repoRoot, StringComparison.OrdinalIgnoreCase) + ? normalizedTestFile.Substring(repoRoot.Length).TrimStart('/') + : normalizedTestFile; + + Assert.Contains( + $"sourcepath={expectedRelativeFile};", + text, + $"AzDO annotation must point at the test method's source file. Got: {text}\nLogs:\n{string.Join("\n", logger.Logs)}"); + Assert.DoesNotContain( + "/src/TestFramework/TestFramework/Assertions/", + text, + $"AzDO annotation must not point at a framework Assert partial-class file. Got: {text}\nLogs:\n{string.Join("\n", logger.Logs)}"); + } + [TestMethod] public void FormatErrorMessage_PlacesTestNameAsTitleOnFirstLine() { diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs new file mode 100644 index 0000000000..3d60e65b9f --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs @@ -0,0 +1,136 @@ +// 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; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +/// +/// Verifies that frames produced by , , +/// and are stripped from on +/// runtimes that honor [StackTraceHidden] (.NET 6+). On older runtimes (.NET +/// Framework / netstandard2.0 consumers), the attribute is a no-op and the frames remain +/// visible; these tests are therefore only meaningful on the modern runtime and are gated +/// accordingly. +/// +public class AssertStackTraceHiddenTests : TestContainer +{ + public void AssertAreEqualFailure_StackTraceHidesFrameworkFrame() + { + AssertFailedException exception = CaptureFailure(() => Assert.AreEqual(1, 2)); + + AssertHidesFrameworkFrames(exception); + } + + public void AssertIComparableFailure_StackTraceHidesFrameworkFrame() + { + AssertFailedException exception = CaptureFailure(() => Assert.IsGreaterThan(1, 0)); + + AssertHidesFrameworkFrames(exception); + } + + public void AssertIsNullFailure_StackTraceHidesFrameworkFrame() + { + AssertFailedException exception = CaptureFailure(() => Assert.IsNull(new object())); + + AssertHidesFrameworkFrames(exception); + } + + public void AssertFailFailure_StackTraceHidesFrameworkFrame() + { + AssertFailedException exception = CaptureFailure(() => Assert.Fail()); + + AssertHidesFrameworkFrames(exception); + } + + public void CollectionAssertFailure_StackTraceHidesFrameworkFrame() + { + AssertFailedException exception = CaptureFailure( + () => CollectionAssert.AreEqual(new[] { 1 }, new[] { 2 })); + + AssertHidesFrameworkFrames(exception); + } + + public void StringAssertFailure_StackTraceHidesFrameworkFrame() + { + AssertFailedException exception = CaptureFailure( + () => StringAssert.Contains("hello", "world")); + + AssertHidesFrameworkFrames(exception); + } + + public void AssertIsNullInterpolatedHandlerFailure_StackTraceHidesFrameworkFrame() + { + int value = 42; + AssertFailedException exception = CaptureFailure( + () => Assert.IsNull(new object(), $"value was {value}")); + + AssertHidesFrameworkFrames(exception); + } + + public void AssertFailureInsideScope_StackTraceHidesFrameworkFrame() + { + AssertFailedException exception = CaptureFailure(() => + { + using (Assert.Scope()) + { + Assert.AreEqual(1, 2); + } + }); + + AssertHidesFrameworkFrames(exception); + } + + private static AssertFailedException CaptureFailure(Action action) + { + try + { + action(); + } + catch (AssertFailedException ex) + { + return ex; + } + + throw new InvalidOperationException("Expected AssertFailedException was not thrown."); + } + + private static void AssertHidesFrameworkFrames(Exception exception) + { +#if NET6_0_OR_GREATER + string? stackTrace = exception.StackTrace; + stackTrace.Should().NotBeNull(); + + // No assertion-framework method should leak into the captured stack trace. + // The runtime should have stripped every method on Assert / CollectionAssert / + // StringAssert / AssertExtensions (and on the nested interpolated string + // handlers and the AssertScope path) because they are all annotated with + // [StackTraceHidden]. + string[] forbiddenTypePrefixes = + [ + "Microsoft.VisualStudio.TestTools.UnitTesting.Assert.", + "Microsoft.VisualStudio.TestTools.UnitTesting.Assert+", + "Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert.", + "Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert.", + "Microsoft.VisualStudio.TestTools.UnitTesting.AssertExtensions.", + "Microsoft.VisualStudio.TestTools.UnitTesting.AssertScope.", + "Microsoft.VisualStudio.TestTools.UnitTesting.StructuredAssertionMessage.", + "Microsoft.VisualStudio.TestTools.UnitTesting.AssertionValueRenderer.", + "Microsoft.VisualStudio.TestTools.UnitTesting.EvidenceBlock.", + ]; + + foreach (string prefix in forbiddenTypePrefixes) + { + stackTrace!.Should().NotContain( + prefix, + because: $"frames whose declaring type starts with '{prefix}' should be hidden by [StackTraceHidden] on .NET 6+ (actual stack trace: {Environment.NewLine}{stackTrace})"); + } +#else + // [StackTraceHidden] is a no-op on .NET Framework / netstandard2.0 hosts. The exception is + // still produced; we just don't assert anything about the stack contents here. + exception.Should().NotBeNull(); +#endif + } +} From e542f45365dab7035a57904991c5c7adb1411bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 20 May 2026 17:49:15 +0200 Subject: [PATCH 2/6] Replace per-API behavioral tests with reflection-based coverage tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walk every public method on Assert/CollectionAssert/StringAssert/AssertExtensions and assert [StackTraceHidden] is reachable (directly or via the declaring type). This means a newly added public assertion API automatically gets verified — no test author needs to remember to add a per-API check. Also verify the nested *InterpolatedStringHandler structs and the internal helpers on the assertion call path (AssertScope, StructuredAssertionMessage, AssertionValueRenderer, EvidenceBlock, EvidenceLine) carry the attribute. Two runtime smoke tests (Assert.AreEqual and Assert.Scope) remain to guard against the polyfill or the BCL silently ceasing to honor the attribute — reflection cannot catch that class of regression. Verified on net9.0 and net48 (the polyfill is detected by FullName, so the test is cross-TFM). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/AssertStackTraceHiddenTests.cs | 212 +++++++++++------- 1 file changed, 133 insertions(+), 79 deletions(-) diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs index 3d60e65b9f..68b6e7900a 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs @@ -1,6 +1,9 @@ // 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; +using System.Reflection; + using AwesomeAssertions; using TestFramework.ForTestingMSTest; @@ -8,81 +11,169 @@ namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; /// -/// Verifies that frames produced by , , -/// and are stripped from on -/// runtimes that honor [StackTraceHidden] (.NET 6+). On older runtimes (.NET -/// Framework / netstandard2.0 consumers), the attribute is a no-op and the frames remain -/// visible; these tests are therefore only meaningful on the modern runtime and are gated -/// accordingly. +/// Guarantees that every public assertion API is covered by [StackTraceHidden], so +/// MSTest framework frames are stripped from on +/// runtimes that honor the attribute (.NET 6+). The reflection-based checks ensure new +/// public APIs added to // +/// in the future cannot silently regress this guarantee, and two runtime smoke tests +/// confirm the attribute is actually honored end-to-end. /// public class AssertStackTraceHiddenTests : TestContainer { - public void AssertAreEqualFailure_StackTraceHidesFrameworkFrame() - { - AssertFailedException exception = CaptureFailure(() => Assert.AreEqual(1, 2)); + // We compare attributes by full type name rather than by Type identity because each + // assembly that needs the polyfill (MSTest.TestFramework, the test assembly itself, …) + // contributes its own internal copy of System.Diagnostics.StackTraceHiddenAttribute on + // pre-.NET 6 TFMs. Their FullName matches; their Type identities do not. + private const string StackTraceHiddenAttributeFullName = "System.Diagnostics.StackTraceHiddenAttribute"; - AssertHidesFrameworkFrames(exception); - } + public void Assert_AllPublicMethodsAreCoveredByStackTraceHidden() + => AssertAllPublicMethodsAreCovered(typeof(Assert)); - public void AssertIComparableFailure_StackTraceHidesFrameworkFrame() - { - AssertFailedException exception = CaptureFailure(() => Assert.IsGreaterThan(1, 0)); + public void CollectionAssert_AllPublicMethodsAreCoveredByStackTraceHidden() + => AssertAllPublicMethodsAreCovered(typeof(CollectionAssert)); - AssertHidesFrameworkFrames(exception); - } + public void StringAssert_AllPublicMethodsAreCoveredByStackTraceHidden() + => AssertAllPublicMethodsAreCovered(typeof(StringAssert)); - public void AssertIsNullFailure_StackTraceHidesFrameworkFrame() + public void AssertExtensions_AllPublicMethodsAreCoveredByStackTraceHidden() { - AssertFailedException exception = CaptureFailure(() => Assert.IsNull(new object())); - - AssertHidesFrameworkFrames(exception); + Type extensionsType = typeof(Assert).Assembly.GetType("Microsoft.VisualStudio.TestTools.UnitTesting.AssertExtensions", throwOnError: true)!; + AssertAllPublicMethodsAreCovered(extensionsType); } - public void AssertFailFailure_StackTraceHidesFrameworkFrame() + public void Assert_NestedInterpolatedStringHandlersAreCoveredByStackTraceHidden() + => AssertNestedHandlerTypesAreCovered(typeof(Assert)); + + public void InternalAssertionHelpers_AreCoveredByStackTraceHidden() { - AssertFailedException exception = CaptureFailure(() => Assert.Fail()); + // These helpers sit between Assert/CollectionAssert/StringAssert and the throw site; + // they need to be hidden so the "hidden span" of frames is contiguous (see #8277). + string[] internalHelperNames = + [ + "Microsoft.VisualStudio.TestTools.UnitTesting.AssertScope", + "Microsoft.VisualStudio.TestTools.UnitTesting.StructuredAssertionMessage", + "Microsoft.VisualStudio.TestTools.UnitTesting.AssertionValueRenderer", + "Microsoft.VisualStudio.TestTools.UnitTesting.EvidenceBlock", + "Microsoft.VisualStudio.TestTools.UnitTesting.EvidenceLine", + ]; - AssertHidesFrameworkFrames(exception); + Assembly assembly = typeof(Assert).Assembly; + foreach (string typeName in internalHelperNames) + { + Type type = assembly.GetType(typeName, throwOnError: true)!; + HasStackTraceHiddenAttribute(type) + .Should() + .BeTrue($"internal helper '{typeName}' should carry [StackTraceHidden] so framework frames between Assert.* and the throw site are contiguously hidden"); + } } - public void CollectionAssertFailure_StackTraceHidesFrameworkFrame() + public void AssertFailure_StackTraceDoesNotContainAnyFrameworkFrameOnNet6Plus() { - AssertFailedException exception = CaptureFailure( - () => CollectionAssert.AreEqual(new[] { 1 }, new[] { 2 })); + // Behavioral smoke test: forces a real assertion failure and confirms the runtime + // actually strips MSTest frames from the captured stack trace. Reflection-only + // checks can't catch issues like a broken polyfill or the BCL ignoring our + // attribute, so we keep this one runtime check as a backstop. + AssertFailedException exception = CaptureFailure(() => Assert.AreEqual(1, 2)); - AssertHidesFrameworkFrames(exception); +#if NET6_0_OR_GREATER + exception.StackTrace.Should().NotBeNull(); + exception.StackTrace! + .Should() + .NotContain("Microsoft.VisualStudio.TestTools.UnitTesting.Assert.", because: $"[StackTraceHidden] should strip Assert frames on .NET 6+. Actual:{Environment.NewLine}{exception.StackTrace}"); +#else + exception.Should().NotBeNull(); +#endif } - public void StringAssertFailure_StackTraceHidesFrameworkFrame() + public void AssertFailureInsideScope_StackTraceDoesNotContainAnyFrameworkFrameOnNet6Plus() { - AssertFailedException exception = CaptureFailure( - () => StringAssert.Contains("hello", "world")); + // Verifies the AssertScope rethrow path also produces a hidden stack on .NET 6+: + // the original failure is captured by ExceptionDispatchInfo and re-thrown from + // AssertScope.Dispose(), so AssertScope itself must also be hidden. + AssertFailedException exception = CaptureFailure(() => + { + using (Assert.Scope()) + { + Assert.AreEqual(1, 2); + } + }); - AssertHidesFrameworkFrames(exception); +#if NET6_0_OR_GREATER + exception.StackTrace.Should().NotBeNull(); + exception.StackTrace! + .Should() + .NotContain("Microsoft.VisualStudio.TestTools.UnitTesting.AssertScope.", because: $"AssertScope.Dispose() must also be hidden. Actual:{Environment.NewLine}{exception.StackTrace}"); + exception.StackTrace! + .Should() + .NotContain("Microsoft.VisualStudio.TestTools.UnitTesting.Assert.", because: $"the captured failure frame itself must remain hidden. Actual:{Environment.NewLine}{exception.StackTrace}"); +#else + exception.Should().NotBeNull(); +#endif } - public void AssertIsNullInterpolatedHandlerFailure_StackTraceHidesFrameworkFrame() + private static void AssertAllPublicMethodsAreCovered(Type type) { - int value = 42; - AssertFailedException exception = CaptureFailure( - () => Assert.IsNull(new object(), $"value was {value}")); + bool typeIsHidden = HasStackTraceHiddenAttribute(type); - AssertHidesFrameworkFrames(exception); + MethodInfo[] publicMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + List uncovered = []; + foreach (MethodInfo method in publicMethods) + { + // Property accessors, operator overloads and compiler-emitted methods do not + // appear in user-visible stack traces; skip them to keep the contract focused on + // surface APIs. + if (method.IsSpecialName) + { + continue; + } + + // Equals/GetHashCode/ToString inherited via 'public static new' overloads on Assert + // are obsolete sentinels; they still go through Fail (hidden) so don't need + // independent annotation, but they are covered for free when the declaring type + // is hidden. Inherited Object methods (no override) aren't returned with + // DeclaredOnly, so nothing to skip here. + if (typeIsHidden || HasStackTraceHiddenAttribute(method)) + { + continue; + } + + uncovered.Add($"{method.DeclaringType!.FullName}.{method.Name}({string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name))})"); + } + + uncovered.Should().BeEmpty( + $"every public method on {type.FullName} must be covered by [StackTraceHidden] (either directly or via its declaring type) so MSTest frames are stripped from user stack traces on .NET 6+. Uncovered: {string.Join(", ", uncovered)}"); } - public void AssertFailureInsideScope_StackTraceHidesFrameworkFrame() + private static void AssertNestedHandlerTypesAreCovered(Type ownerType) { - AssertFailedException exception = CaptureFailure(() => + Type[] nestedTypes = ownerType.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); + Type[] handlers = nestedTypes + .Where(static t => t.Name.EndsWith("InterpolatedStringHandler", StringComparison.Ordinal)) + .ToArray(); + + // Sanity check: the assertion API exposes several InterpolatedStringHandler types; if + // this drops to zero we've likely broken our reflection assumptions rather than + // actually removed every handler. + handlers.Should().NotBeEmpty($"{ownerType.FullName} is expected to expose nested *InterpolatedStringHandler structs"); + + List uncovered = []; + foreach (Type handler in handlers) { - using (Assert.Scope()) + if (!HasStackTraceHiddenAttribute(handler)) { - Assert.AreEqual(1, 2); + uncovered.Add(handler.FullName ?? handler.Name); } - }); + } - AssertHidesFrameworkFrames(exception); + uncovered.Should().BeEmpty( + $"every nested *InterpolatedStringHandler under {ownerType.FullName} must carry [StackTraceHidden] (nested types have their own DeclaringType, so the outer type's attribute does not propagate). Uncovered: {string.Join(", ", uncovered)}"); } + private static bool HasStackTraceHiddenAttribute(MemberInfo member) + => member.GetCustomAttributesData() + .Any(static attr => attr.AttributeType.FullName == StackTraceHiddenAttributeFullName); + private static AssertFailedException CaptureFailure(Action action) { try @@ -96,41 +187,4 @@ private static AssertFailedException CaptureFailure(Action action) throw new InvalidOperationException("Expected AssertFailedException was not thrown."); } - - private static void AssertHidesFrameworkFrames(Exception exception) - { -#if NET6_0_OR_GREATER - string? stackTrace = exception.StackTrace; - stackTrace.Should().NotBeNull(); - - // No assertion-framework method should leak into the captured stack trace. - // The runtime should have stripped every method on Assert / CollectionAssert / - // StringAssert / AssertExtensions (and on the nested interpolated string - // handlers and the AssertScope path) because they are all annotated with - // [StackTraceHidden]. - string[] forbiddenTypePrefixes = - [ - "Microsoft.VisualStudio.TestTools.UnitTesting.Assert.", - "Microsoft.VisualStudio.TestTools.UnitTesting.Assert+", - "Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert.", - "Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert.", - "Microsoft.VisualStudio.TestTools.UnitTesting.AssertExtensions.", - "Microsoft.VisualStudio.TestTools.UnitTesting.AssertScope.", - "Microsoft.VisualStudio.TestTools.UnitTesting.StructuredAssertionMessage.", - "Microsoft.VisualStudio.TestTools.UnitTesting.AssertionValueRenderer.", - "Microsoft.VisualStudio.TestTools.UnitTesting.EvidenceBlock.", - ]; - - foreach (string prefix in forbiddenTypePrefixes) - { - stackTrace!.Should().NotContain( - prefix, - because: $"frames whose declaring type starts with '{prefix}' should be hidden by [StackTraceHidden] on .NET 6+ (actual stack trace: {Environment.NewLine}{stackTrace})"); - } -#else - // [StackTraceHidden] is a no-op on .NET Framework / netstandard2.0 hosts. The exception is - // still produced; we just don't assert anything about the stack contents here. - exception.Should().NotBeNull(); -#endif - } } From 2da2dd7b3b1e57b06fc97940a248eb7c3b1d1813 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 19:17:16 +0200 Subject: [PATCH 3/6] Fix RealAssertFailure test: handle deterministic build path and defang ##vso in failure messages The test was failing on CI because: 1. With true, Roslyn rewrites source paths via /pathmap, so [CallerFilePath] returns '/_/test/...'. The AzDO reporter strips that prefix and emits 'sourcepath=test/...', but the test was building 'expectedRelativeFile' by only stripping the runtime repo root, leaving '/_/test/...' as the expected substring -- which never matched. 2. When the Assert.Contains/DoesNotContain failure messages embedded the raw '##vso[task.logissue...]' text, AzDO scanned the test output, saw '##vso[' and tried to interpret it as a real command, surfacing 'Unable to process command ... successfully'. Fix: mirror the same '/_/' -> '' stripping as the reporter, and trim the leading '##' from the captured text plus strip '##vso[' from the logger trace output before embedding either in failure messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureDevOpsTests.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsTests.cs index e912fa3249..3e52e9d969 100644 --- a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsTests.cs +++ b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsTests.cs @@ -231,6 +231,7 @@ public void RealAssertFailure_AnnotationPointsAtTestMethodAndNotAtFrameworkFile( } var logger = new TextLogger(); + // Trim ##. If we keep it, then when the test fails, the assertion failure will get printed to screen and picked up incorrectly by AzDO, because it scans all output for the ##vso... pattern. string? text = AzureDevOpsReporter.GetErrorText( "MyTestDisplayName", explanation: null, @@ -238,24 +239,38 @@ public void RealAssertFailure_AnnotationPointsAtTestMethodAndNotAtFrameworkFile( "severity", new SystemFileSystem(), logger, - "net9.0"); - - Assert.IsNotNull(text, $"AzDO reporter should have produced an annotation. Logs:\n{string.Join("\n", logger.Logs)}"); - + "net9.0")?.TrimStart('#'); + + // Also neutralize any '##vso[' that the trace logger captured (the reporter logs the full + // command line at trace level) so the assertion failure messages below cannot smuggle a + // real AzDO command into the build output when this test fails on CI. + string safeLogs = string.Join("\n", logger.Logs).Replace("##vso[", "vso["); + + Assert.IsNotNull(text, $"AzDO reporter should have produced an annotation. Logs:\n{safeLogs}"); + + // Mirror the path normalization performed by AzureDevOpsReporter.GetErrorText: + // * Strip the deterministic-build root '/_/' (set by true + // in CI), then fall back to stripping the runtime repo root for local builds. + // * Normalize separators to '/'. + // Without this, the test breaks in CI where [CallerFilePath] returns '/_/test/...' + // but the reporter strips '/_/' and emits 'sourcepath=test/...'. + const string DeterministicBuildRoot = "/_/"; string normalizedTestFile = testFile.Replace('\\', '/'); string repoRoot = RootFinder.Find().Replace('\\', '/'); - string expectedRelativeFile = normalizedTestFile.StartsWith(repoRoot, StringComparison.OrdinalIgnoreCase) - ? normalizedTestFile.Substring(repoRoot.Length).TrimStart('/') - : normalizedTestFile; + string expectedRelativeFile = normalizedTestFile.StartsWith(DeterministicBuildRoot, StringComparison.OrdinalIgnoreCase) + ? normalizedTestFile.Substring(DeterministicBuildRoot.Length) + : normalizedTestFile.StartsWith(repoRoot, StringComparison.OrdinalIgnoreCase) + ? normalizedTestFile.Substring(repoRoot.Length).TrimStart('/') + : normalizedTestFile; Assert.Contains( $"sourcepath={expectedRelativeFile};", text, - $"AzDO annotation must point at the test method's source file. Got: {text}\nLogs:\n{string.Join("\n", logger.Logs)}"); + $"AzDO annotation must point at the test method's source file. Got: {text}\nLogs:\n{safeLogs}"); Assert.DoesNotContain( "/src/TestFramework/TestFramework/Assertions/", text, - $"AzDO annotation must not point at a framework Assert partial-class file. Got: {text}\nLogs:\n{string.Join("\n", logger.Logs)}"); + $"AzDO annotation must not point at a framework Assert partial-class file. Got: {text}\nLogs:\n{safeLogs}"); } [TestMethod] From b7ca97423abed48a771495b9c1713668e012e362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 21 May 2026 10:46:34 +0200 Subject: [PATCH 4/6] Cover generic interpolated string handlers in stack trace test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/AssertStackTraceHiddenTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs index 68b6e7900a..71bcb371c0 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs @@ -149,7 +149,7 @@ private static void AssertNestedHandlerTypesAreCovered(Type ownerType) { Type[] nestedTypes = ownerType.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); Type[] handlers = nestedTypes - .Where(static t => t.Name.EndsWith("InterpolatedStringHandler", StringComparison.Ordinal)) + .Where(static t => t.Name.Contains("InterpolatedStringHandler", StringComparison.Ordinal)) .ToArray(); // Sanity check: the assertion API exposes several InterpolatedStringHandler types; if From c2e6b537b0c7eb54a5099b0768c09e5fa0b08547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 21 May 2026 18:01:28 +0200 Subject: [PATCH 5/6] Address PR review comments and fix build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureDevOpsReporter.cs | 2 +- .../Assertions/AssertStackTraceHiddenTests.cs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs index ddd2e6d540..c27425b91e 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs @@ -258,7 +258,7 @@ private async Task WriteExceptionAsync(string testDisplayName, string testName, logger.LogTrace($"Using relative path '{relativePath}'."); } } - else if (file.StartsWith(repoRoot, StringComparison.CurrentCultureIgnoreCase)) + else if (file.StartsWith(repoRoot, StringComparison.OrdinalIgnoreCase)) { if (logger.IsEnabled(LogLevel.Trace)) { diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs index 71bcb371c0..3484313f21 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs @@ -158,12 +158,9 @@ private static void AssertNestedHandlerTypesAreCovered(Type ownerType) handlers.Should().NotBeEmpty($"{ownerType.FullName} is expected to expose nested *InterpolatedStringHandler structs"); List uncovered = []; - foreach (Type handler in handlers) + foreach (Type handler in handlers.Where(static handler => !HasStackTraceHiddenAttribute(handler))) { - if (!HasStackTraceHiddenAttribute(handler)) - { - uncovered.Add(handler.FullName ?? handler.Name); - } + uncovered.Add(handler.FullName ?? handler.Name); } uncovered.Should().BeEmpty( From 3d31058b474a28617013dfbe1a68e34043477252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 21 May 2026 19:36:21 +0200 Subject: [PATCH 6/6] Fix soft assertion acceptance regex Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs index 4227f684eb..aa247c3f30 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs @@ -41,7 +41,7 @@ public async Task ScopeWithMultipleFailures_TestFailsWithAggregatedMessage() // 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]+at Microsoft\.VisualStudio\.TestTools\.UnitTesting\.AssertScope\.Dispose\(\)[\s\S]+at UnitTest1\.ScopeWithMultipleFailures\(\)"""); + """failed ScopeWithMultipleFailures \(\d+ms\)[\s\S]+2 assertion\(s\) failed within the assert scope\.[\s\S]+at UnitTest1\.ScopeWithMultipleFailures\(\)"""); } [TestMethod]