Apply [StackTraceHidden] to Assert/CollectionAssert/StringAssert (#8277)#8393
Apply [StackTraceHidden] to Assert/CollectionAssert/StringAssert (#8277)#8393Evangelink wants to merge 3 commits into
Conversation
#8277) 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>
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>
Evangelink
left a comment
There was a problem hiding this comment.
Code Review — PR #8393: Apply [StackTraceHidden] to Assert/CollectionAssert/StringAssert
⚠️ Important: Diff presented for review does not match actual PR branch HEAD
The "Full PR Diff" provided in this review request is substantially inaccurate relative to refs/pull/8393/head. I read the actual files from the PR branch to verify. Here is what I found:
Diff vs Reality — Key Discrepancies
| Claim in provided diff | Actual state in PR branch HEAD |
|---|---|
Assert.cs, CollectionAssert.cs, StringAssert.cs already had [DebuggerNonUserCode] + [StackTraceHidden] before PR |
Main branch has neither attribute at class level |
PR adds a duplicate [StackTraceHidden] to those three files |
PR adds exactly one [StackTraceHidden] to each — clean |
AssertScope.cs already had [StackTraceHidden] — PR duplicates it |
PR adds exactly one [StackTraceHidden] — clean |
Test file uses [TestClass]/[TestMethod] MSTest attributes with #if NET |
Actual test file uses TestContainer base class (internal framework), AwesomeAssertions, #if NET6_0_OR_GREATER |
Project file modified is Microsoft.TestFramework.UnitTests.csproj with inline XML comment |
That file does not exist; actual project TestFramework.UnitTests.csproj is not changed by this PR |
Review of the Actual PR (from refs/pull/8393/head)
Correctness ✅
Every assertion type gets exactly one [StackTraceHidden]. No duplicates. Nested interpolated-string handler structs are annotated individually (required because they are separate DeclaringTypes). Internal helpers on the call path (AssertScope, StructuredAssertionMessage, AssertionValueRenderer, EvidenceBlock, EvidenceLine) are annotated. The [StackTraceHidden] on EvidenceBlock/EvidenceLine readonly structs is harmless — those structs' methods won't appear in stack traces under normal usage, but the annotation is defensive and costs nothing.
[DebuggerNonUserCode] removal ✅
The main branch never had [DebuggerNonUserCode] at the class level on Assert, CollectionAssert, or StringAssert. The PR does not add or remove it. No debugger-stepping regression.
AssertExtensions (internal static) ✅
[StackTraceHidden] applies to static classes and their methods. Extension methods defined there will be hidden correctly.
Tests ✅
The actual test file (AssertStackTraceHiddenTests.cs) correctly:
- Extends
TestContainer(as required by the internal test framework) - Uses
AwesomeAssertionsfor assertions - Gates the stack-trace content assertions under
#if NET6_0_OR_GREATER— more precise than#if NET - Covers
Assert,CollectionAssert,StringAssert, an interpolated-handler variant, and theAssertScopepath
Cross-TFM ✅
The polyfill at src/Polyfills/StackTraceHiddenAttribute.cs provides the attribute on netstandard2.0/net462 (no-op). No behavioral change on .NET Framework.
Public API ✅
[StackTraceHidden] does not surface in API signatures. PublicAPI.*.txt files correctly unaffected.
Verdict
| Dimension | Result |
|---|---|
| Correctness | ✅ |
| Duplicate attributes | ✅ None in actual code |
| Cross-TFM compatibility | ✅ |
| Test quality | ✅ |
| Public API surface | ✅ Unaffected |
| Debugger experience | ✅ No regression |
The implementation in the actual PR branch is clean and well-considered. No blocking issues found. Note to PR author: the diff representation provided to reviewers was misleading; consider verifying the diff source used for review preparation.
Generated by Expert Code Review (on open) for issue #8393 · ● 10.6M
…g ##vso in failure messages The test was failing on CI because: 1. With <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>, 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>
| { | ||
| Type[] nestedTypes = ownerType.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); | ||
| Type[] handlers = nestedTypes | ||
| .Where(static t => t.Name.EndsWith("InterpolatedStringHandler", StringComparison.Ordinal)) |
Fixes #8277.
What
Annotate every public assertion type with [StackTraceHidden] so the .NET 6+ runtime omits MSTest framework frames from Exception.StackTrace. Coverage:
Assert.Thatextension surface)AssertScope,StructuredAssertionMessage,EvidenceBlock,EvidenceLine,AssertionValueRendererAssert*InterpolatedStringHandlerstruct (those have their ownDeclaringType, so the outerAssert's attribute does not propagate)The polyfill at
src/Polyfills/StackTraceHiddenAttribute.csalready provides the attribute as an embeddedinternaltype on netstandard2.0/net462 (no-op there) and forwards to the BCL type on .NET 6+.Why
Tests
test/UnitTests/TestFramework.UnitTests/Assertions/AssertStackTraceHiddenTests.cs— eight tests that force a real assertion failure (AreEqual, IsGreaterThan, IsNull, Fail, CollectionAssert.AreEqual, StringAssert.Contains, an interpolated-handler variant, and a failure collected viaAssert.Scope()) and verify on .NET 6+ that the capturedException.StackTracecontains no MSTest framework frame.RealAssertFailure_AnnotationPointsAtTestMethodAndNotAtFrameworkFileinAzureDevOpsTests.cs— end-to-end test that exercises a realAssert.AreEqual(1, 2)failure throughAzureDevOpsReporter.GetErrorTextand asserts the emittedsourcepathpoints at the test method's source file rather than any framework partial-class file.Notes
[StackTraceHidden]is a no-op on netstandard2.0 / .NET Framework consumers; the AzDO reporter's existing type-prefix heuristic continues to cover that case.[StackTraceHidden]annotations insideAssert.cs/Assert.ThrowsException.csare now redundant under the type-level attribute but were left in place to keep the diff minimal.PublicAPI.*.txtfiles are unaffected because[StackTraceHidden]does not surface in API signatures.