Add Assert.AddValueFormatter for customizing assertion failure rendering - #9148

Merged
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089
Jun 16, 2026
Merged

Add Assert.AddValueFormatter for customizing assertion failure rendering#9148
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#9089

Adds two public Assert.AddValueFormatter overloads that let users register custom formatters used by the structured assertion failure message renderer introduced by RFC-012 (#8964):

  • AddValueFormatter<T>(Func<T, string?>) — typed sugar. Return null to fall through to the next formatter / built-in renderer.
  • AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>) — chain-of-responsibility factory that receives the next formatter.

Both overloads return an IDisposable whose disposal removes the registration. Registrations are anchored on AsyncLocal<T>, so the scope follows the test context (per-test, per-class, per-assembly), and they stack newest-first so the most recently registered formatter wins.

Example

[TestMethod]publicvoidDateTimeFailureShowsKind(){usingvar_=Assert.AddValueFormatter<DateTime>(dt =>$""{dt:O} [{dt.Kind}]"");Assert.AreEqual(d1,d2);// failure message now annotates DateTime.Kind}

Implementation

  • New AssertionValueFormatterRegistry (internal, AsyncLocal-backed) holds the chain. Nodes are append-only with a mutable IsRemoved flag so Dispose works for any position in the stack (including out-of-order disposal).
  • AssertionValueRenderer.RenderValue consults the registry ahead of the built-in switch, and only when at least one formatter is registered (zero-allocation fast-path for the common no-formatter case).
  • null is always rendered as ""null"" and is never exposed to user formatters, to avoid spurious NullReferenceExceptions inside user code.
  • User-formatter exceptions are caught and annotated so the underlying assertion failure remains useful.
  • New API is marked [Experimental(""MSTESTEXP"")] consistent with Assert.Scope().

Tests

15 new AssertTests.AddValueFormatter tests cover:

  • ArgumentNullException on both overloads
  • End-to-end assertion failure-message customization
  • Disposal removes the registration (including out-of-order disposal)
  • Idempotent Dispose
  • Newest-first stacking + fall-through ordering
  • Null values bypass the registry
  • Non-matching types bypass the typed overload
  • Collection items get per-item formatting
  • The factory overload composes with the typed overload
  • Throwing formatters do not derail rendering

Verified passing on net9.0, net8.0, and net48.

Out of scope (per the issue)

  • No discovery via attributes/interfaces (deferred).
  • No built-in defaults changed.
  • No equality customization (existing IEqualityComparer<T> overloads cover that).

…ing (#9089)
Adds two public `Assert.AddValueFormatter` overloads that let users register
custom formatters used by the structured assertion failure message renderer:
* `AddValueFormatter<T>(Func<T, string?>)` -- typed sugar.
* `AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>)`
-- chain-of-responsibility factory receiving the next formatter.
Both overloads return an `IDisposable` that removes the registration.
Registrations are anchored on `AsyncLocal<T>` so the scope follows the test
context (per-test, per-class, per-assembly), and they stack newest-first
with null-from-typed-formatter meaning "fall through". Null values continue to
render as `null` and are not exposed to user formatters.
Internally adds `AssertionValueFormatterRegistry` (the AsyncLocal-backed chain)
and wires it into `AssertionValueRenderer.RenderValue` ahead of the built-in
switch so all assertions that surface values through structured messages pick it
up automatically. User-formatter exceptions are caught and annotated to keep the
underlying assertion failure useful.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 11:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental public API to MSTest (Assert.AddValueFormatter) to let users customize how values are rendered in the structured assertion failure messages (via an AsyncLocal-scoped, newest-first formatter stack), and updates the central renderer to consult those formatters before the built-in formatting logic.

Changes:

  • Introduces Assert.AddValueFormatter<T>(Func<T, string?>) and Assert.AddValueFormatter(Func<Func<object?,string>, Func<object?,string>>) returning IDisposable for scoped registration/removal.
  • Adds an internal AssertionValueFormatterRegistry (AsyncLocal-backed) and wires it into AssertionValueRenderer.RenderValue.
  • Adds unit tests covering registration, stacking, disposal semantics, null bypass, and failure-message customization.
Show a summary per file
FileDescription
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AddValueFormatter.csNew unit tests for both overloads, ordering, disposal, and rendering behavior.
src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txtDeclares the new experimental public API signatures.
src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.csConsults the formatter registry before built-in rendering (with a no-formatters fast path).
src/TestFramework/TestFramework/Assertions/AssertionValueFormatterRegistry.csNew internal AsyncLocal registry that composes formatter factories and supports removal via disposal.
src/TestFramework/TestFramework/Assertions/Assert.AddValueFormatter.csImplements the two new public overloads and documents scoping/stacking semantics.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

- Move chain construction inside the try/catch so a throwing user factory
falls back to the built-in renderer instead of escaping.
- Mark Node.IsRemoved volatile so out-of-flow disposal is reliably observed
across threads.
- Inline the chain local to drop the redundant assignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

Prove, against a real MSTest process, that Assert.AddValueFormatter
registrations flow down the AssemblyInitialize -> ClassInitialize ->
TestMethod lifecycle:
- an assembly-scoped formatter is visible in tests,
- a class-scoped formatter wins over and layers on top of the assembly one,
- a test-local registration applies within its test and does not leak into
sibling tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 12:44

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

…cation docs
Addresses PR review feedback:
- AssertionValueFormatterRegistry now prunes leading removed nodes and
resets the AsyncLocal head, so HasFormatters returns to false once every
registration in the flow is disposed (restoring the zero-allocation
fast-path instead of traversing removed nodes on every subsequent render).
- Document that the factory overload's delegate is the chain builder and is
invoked on every render while active, not once at registration time.
- Add a unit test asserting the fast-path is restored after disposing all
registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

The Windows Debug failure was an unrelated, intermittent infra flake in MSBuildTests_KnownExtensionRegistration.Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered: 2 of the 4 net10.0 matrix entries threw Sequence contains no matching element because the _GenerateSelfRegisteredExtensions target was absent from the captured binlog, while the other 2 net10.0 entries (and all other TFMs) passed. This PR only touches Assert code in TestFramework and cannot affect MSBuild extension registration. Pushed an empty commit to re-run CI.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9148

17 test methods graded across 2 files; all 17 earn A (90–100). Tests are well-named, single-behavior, and cover the formatter registry's lifecycle end-to-end — null guards, type specificity, LIFO stacking, fall-through chaining, dispose idempotency, fast-path restoration, fault isolation, and acceptance-level scope flow-down. No issues found across the board.

ΔTestGradeBandNotes
newValueFormatterFlowTests.
ValueFormatterScopesFlowDownCorrectly
A90–100No issues found.
newAssertTests.
AddValueFormatter_
AppliesToCollectionItems
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeIsIdempotent
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeRemovesOnlyThatRegistration
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposingAllRegistrationsRestoresFastPath
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ChainsWithTypedOverload
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ReceivesNextDelegate
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Factory_
NullFactory_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
NullValuesAlwaysRenderedAsNullLiteral
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksFallThroughInOrder
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksNewestFirst
A90–100No issues found.
newAssertTests.
AddValueFormatter_
ThrowingFormatterDoesNotDerailRendering
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
CustomizesFailureMessage
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
DoesNotMatchUnrelatedTypes
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
FormatterRemovedAfterDispose
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
NullFormatter_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
ReturningNullFallsThroughToBuiltIn
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 286.5 AIC · ⌖ 13.4 AIC · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 49d5c1b into mainJun 16, 2026
33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/add-value-formatter-9089 branch June 16, 2026 08:00
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 17, 2026
…#9176`, `#9177` (#9207)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Public API for registering custom value formatters used by assertion failure messages

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add Assert.AddValueFormatter for customizing assertion failure rendering - #9148

Merged
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089
Jun 16, 2026
Merged

Add Assert.AddValueFormatter for customizing assertion failure rendering#9148
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#9089

Adds two public Assert.AddValueFormatter overloads that let users register custom formatters used by the structured assertion failure message renderer introduced by RFC-012 (#8964):

  • AddValueFormatter<T>(Func<T, string?>) — typed sugar. Return null to fall through to the next formatter / built-in renderer.
  • AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>) — chain-of-responsibility factory that receives the next formatter.

Both overloads return an IDisposable whose disposal removes the registration. Registrations are anchored on AsyncLocal<T>, so the scope follows the test context (per-test, per-class, per-assembly), and they stack newest-first so the most recently registered formatter wins.

Example

[TestMethod]publicvoidDateTimeFailureShowsKind(){usingvar_=Assert.AddValueFormatter<DateTime>(dt =>$""{dt:O} [{dt.Kind}]"");Assert.AreEqual(d1,d2);// failure message now annotates DateTime.Kind}

Implementation

  • New AssertionValueFormatterRegistry (internal, AsyncLocal-backed) holds the chain. Nodes are append-only with a mutable IsRemoved flag so Dispose works for any position in the stack (including out-of-order disposal).
  • AssertionValueRenderer.RenderValue consults the registry ahead of the built-in switch, and only when at least one formatter is registered (zero-allocation fast-path for the common no-formatter case).
  • null is always rendered as ""null"" and is never exposed to user formatters, to avoid spurious NullReferenceExceptions inside user code.
  • User-formatter exceptions are caught and annotated so the underlying assertion failure remains useful.
  • New API is marked [Experimental(""MSTESTEXP"")] consistent with Assert.Scope().

Tests

15 new AssertTests.AddValueFormatter tests cover:

  • ArgumentNullException on both overloads
  • End-to-end assertion failure-message customization
  • Disposal removes the registration (including out-of-order disposal)
  • Idempotent Dispose
  • Newest-first stacking + fall-through ordering
  • Null values bypass the registry
  • Non-matching types bypass the typed overload
  • Collection items get per-item formatting
  • The factory overload composes with the typed overload
  • Throwing formatters do not derail rendering

Verified passing on net9.0, net8.0, and net48.

Out of scope (per the issue)

  • No discovery via attributes/interfaces (deferred).
  • No built-in defaults changed.
  • No equality customization (existing IEqualityComparer<T> overloads cover that).

…ing (#9089)
Adds two public `Assert.AddValueFormatter` overloads that let users register
custom formatters used by the structured assertion failure message renderer:
* `AddValueFormatter<T>(Func<T, string?>)` -- typed sugar.
* `AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>)`
-- chain-of-responsibility factory receiving the next formatter.
Both overloads return an `IDisposable` that removes the registration.
Registrations are anchored on `AsyncLocal<T>` so the scope follows the test
context (per-test, per-class, per-assembly), and they stack newest-first
with null-from-typed-formatter meaning "fall through". Null values continue to
render as `null` and are not exposed to user formatters.
Internally adds `AssertionValueFormatterRegistry` (the AsyncLocal-backed chain)
and wires it into `AssertionValueRenderer.RenderValue` ahead of the built-in
switch so all assertions that surface values through structured messages pick it
up automatically. User-formatter exceptions are caught and annotated to keep the
underlying assertion failure useful.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 11:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental public API to MSTest (Assert.AddValueFormatter) to let users customize how values are rendered in the structured assertion failure messages (via an AsyncLocal-scoped, newest-first formatter stack), and updates the central renderer to consult those formatters before the built-in formatting logic.

Changes:

  • Introduces Assert.AddValueFormatter<T>(Func<T, string?>) and Assert.AddValueFormatter(Func<Func<object?,string>, Func<object?,string>>) returning IDisposable for scoped registration/removal.
  • Adds an internal AssertionValueFormatterRegistry (AsyncLocal-backed) and wires it into AssertionValueRenderer.RenderValue.
  • Adds unit tests covering registration, stacking, disposal semantics, null bypass, and failure-message customization.
Show a summary per file
FileDescription
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AddValueFormatter.csNew unit tests for both overloads, ordering, disposal, and rendering behavior.
src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txtDeclares the new experimental public API signatures.
src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.csConsults the formatter registry before built-in rendering (with a no-formatters fast path).
src/TestFramework/TestFramework/Assertions/AssertionValueFormatterRegistry.csNew internal AsyncLocal registry that composes formatter factories and supports removal via disposal.
src/TestFramework/TestFramework/Assertions/Assert.AddValueFormatter.csImplements the two new public overloads and documents scoping/stacking semantics.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

- Move chain construction inside the try/catch so a throwing user factory
falls back to the built-in renderer instead of escaping.
- Mark Node.IsRemoved volatile so out-of-flow disposal is reliably observed
across threads.
- Inline the chain local to drop the redundant assignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

Prove, against a real MSTest process, that Assert.AddValueFormatter
registrations flow down the AssemblyInitialize -> ClassInitialize ->
TestMethod lifecycle:
- an assembly-scoped formatter is visible in tests,
- a class-scoped formatter wins over and layers on top of the assembly one,
- a test-local registration applies within its test and does not leak into
sibling tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 12:44

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

…cation docs
Addresses PR review feedback:
- AssertionValueFormatterRegistry now prunes leading removed nodes and
resets the AsyncLocal head, so HasFormatters returns to false once every
registration in the flow is disposed (restoring the zero-allocation
fast-path instead of traversing removed nodes on every subsequent render).
- Document that the factory overload's delegate is the chain builder and is
invoked on every render while active, not once at registration time.
- Add a unit test asserting the fast-path is restored after disposing all
registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

The Windows Debug failure was an unrelated, intermittent infra flake in MSBuildTests_KnownExtensionRegistration.Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered: 2 of the 4 net10.0 matrix entries threw Sequence contains no matching element because the _GenerateSelfRegisteredExtensions target was absent from the captured binlog, while the other 2 net10.0 entries (and all other TFMs) passed. This PR only touches Assert code in TestFramework and cannot affect MSBuild extension registration. Pushed an empty commit to re-run CI.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9148

17 test methods graded across 2 files; all 17 earn A (90–100). Tests are well-named, single-behavior, and cover the formatter registry's lifecycle end-to-end — null guards, type specificity, LIFO stacking, fall-through chaining, dispose idempotency, fast-path restoration, fault isolation, and acceptance-level scope flow-down. No issues found across the board.

ΔTestGradeBandNotes
newValueFormatterFlowTests.
ValueFormatterScopesFlowDownCorrectly
A90–100No issues found.
newAssertTests.
AddValueFormatter_
AppliesToCollectionItems
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeIsIdempotent
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeRemovesOnlyThatRegistration
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposingAllRegistrationsRestoresFastPath
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ChainsWithTypedOverload
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ReceivesNextDelegate
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Factory_
NullFactory_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
NullValuesAlwaysRenderedAsNullLiteral
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksFallThroughInOrder
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksNewestFirst
A90–100No issues found.
newAssertTests.
AddValueFormatter_
ThrowingFormatterDoesNotDerailRendering
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
CustomizesFailureMessage
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
DoesNotMatchUnrelatedTypes
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
FormatterRemovedAfterDispose
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
NullFormatter_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
ReturningNullFallsThroughToBuiltIn
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 286.5 AIC · ⌖ 13.4 AIC · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 49d5c1b into mainJun 16, 2026
33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/add-value-formatter-9089 branch June 16, 2026 08:00
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 17, 2026
…#9176`, `#9177` (#9207)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Public API for registering custom value formatters used by assertion failure messages

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Assert.AddValueFormatter for customizing assertion failure rendering - #9148

Merged
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089
Jun 16, 2026
Merged

Add Assert.AddValueFormatter for customizing assertion failure rendering#9148
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#9089

Adds two public Assert.AddValueFormatter overloads that let users register custom formatters used by the structured assertion failure message renderer introduced by RFC-012 (#8964):

  • AddValueFormatter<T>(Func<T, string?>) — typed sugar. Return null to fall through to the next formatter / built-in renderer.
  • AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>) — chain-of-responsibility factory that receives the next formatter.

Both overloads return an IDisposable whose disposal removes the registration. Registrations are anchored on AsyncLocal<T>, so the scope follows the test context (per-test, per-class, per-assembly), and they stack newest-first so the most recently registered formatter wins.

Example

[TestMethod]publicvoidDateTimeFailureShowsKind(){usingvar_=Assert.AddValueFormatter<DateTime>(dt =>$""{dt:O} [{dt.Kind}]"");Assert.AreEqual(d1,d2);// failure message now annotates DateTime.Kind}

Implementation

  • New AssertionValueFormatterRegistry (internal, AsyncLocal-backed) holds the chain. Nodes are append-only with a mutable IsRemoved flag so Dispose works for any position in the stack (including out-of-order disposal).
  • AssertionValueRenderer.RenderValue consults the registry ahead of the built-in switch, and only when at least one formatter is registered (zero-allocation fast-path for the common no-formatter case).
  • null is always rendered as ""null"" and is never exposed to user formatters, to avoid spurious NullReferenceExceptions inside user code.
  • User-formatter exceptions are caught and annotated so the underlying assertion failure remains useful.
  • New API is marked [Experimental(""MSTESTEXP"")] consistent with Assert.Scope().

Tests

15 new AssertTests.AddValueFormatter tests cover:

  • ArgumentNullException on both overloads
  • End-to-end assertion failure-message customization
  • Disposal removes the registration (including out-of-order disposal)
  • Idempotent Dispose
  • Newest-first stacking + fall-through ordering
  • Null values bypass the registry
  • Non-matching types bypass the typed overload
  • Collection items get per-item formatting
  • The factory overload composes with the typed overload
  • Throwing formatters do not derail rendering

Verified passing on net9.0, net8.0, and net48.

Out of scope (per the issue)

  • No discovery via attributes/interfaces (deferred).
  • No built-in defaults changed.
  • No equality customization (existing IEqualityComparer<T> overloads cover that).

…ing (#9089)
Adds two public `Assert.AddValueFormatter` overloads that let users register
custom formatters used by the structured assertion failure message renderer:
* `AddValueFormatter<T>(Func<T, string?>)` -- typed sugar.
* `AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>)`
-- chain-of-responsibility factory receiving the next formatter.
Both overloads return an `IDisposable` that removes the registration.
Registrations are anchored on `AsyncLocal<T>` so the scope follows the test
context (per-test, per-class, per-assembly), and they stack newest-first
with null-from-typed-formatter meaning "fall through". Null values continue to
render as `null` and are not exposed to user formatters.
Internally adds `AssertionValueFormatterRegistry` (the AsyncLocal-backed chain)
and wires it into `AssertionValueRenderer.RenderValue` ahead of the built-in
switch so all assertions that surface values through structured messages pick it
up automatically. User-formatter exceptions are caught and annotated to keep the
underlying assertion failure useful.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 11:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental public API to MSTest (Assert.AddValueFormatter) to let users customize how values are rendered in the structured assertion failure messages (via an AsyncLocal-scoped, newest-first formatter stack), and updates the central renderer to consult those formatters before the built-in formatting logic.

Changes:

  • Introduces Assert.AddValueFormatter<T>(Func<T, string?>) and Assert.AddValueFormatter(Func<Func<object?,string>, Func<object?,string>>) returning IDisposable for scoped registration/removal.
  • Adds an internal AssertionValueFormatterRegistry (AsyncLocal-backed) and wires it into AssertionValueRenderer.RenderValue.
  • Adds unit tests covering registration, stacking, disposal semantics, null bypass, and failure-message customization.
Show a summary per file
FileDescription
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AddValueFormatter.csNew unit tests for both overloads, ordering, disposal, and rendering behavior.
src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txtDeclares the new experimental public API signatures.
src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.csConsults the formatter registry before built-in rendering (with a no-formatters fast path).
src/TestFramework/TestFramework/Assertions/AssertionValueFormatterRegistry.csNew internal AsyncLocal registry that composes formatter factories and supports removal via disposal.
src/TestFramework/TestFramework/Assertions/Assert.AddValueFormatter.csImplements the two new public overloads and documents scoping/stacking semantics.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

- Move chain construction inside the try/catch so a throwing user factory
falls back to the built-in renderer instead of escaping.
- Mark Node.IsRemoved volatile so out-of-flow disposal is reliably observed
across threads.
- Inline the chain local to drop the redundant assignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

Prove, against a real MSTest process, that Assert.AddValueFormatter
registrations flow down the AssemblyInitialize -> ClassInitialize ->
TestMethod lifecycle:
- an assembly-scoped formatter is visible in tests,
- a class-scoped formatter wins over and layers on top of the assembly one,
- a test-local registration applies within its test and does not leak into
sibling tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 12:44

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

…cation docs
Addresses PR review feedback:
- AssertionValueFormatterRegistry now prunes leading removed nodes and
resets the AsyncLocal head, so HasFormatters returns to false once every
registration in the flow is disposed (restoring the zero-allocation
fast-path instead of traversing removed nodes on every subsequent render).
- Document that the factory overload's delegate is the chain builder and is
invoked on every render while active, not once at registration time.
- Add a unit test asserting the fast-path is restored after disposing all
registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

The Windows Debug failure was an unrelated, intermittent infra flake in MSBuildTests_KnownExtensionRegistration.Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered: 2 of the 4 net10.0 matrix entries threw Sequence contains no matching element because the _GenerateSelfRegisteredExtensions target was absent from the captured binlog, while the other 2 net10.0 entries (and all other TFMs) passed. This PR only touches Assert code in TestFramework and cannot affect MSBuild extension registration. Pushed an empty commit to re-run CI.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9148

17 test methods graded across 2 files; all 17 earn A (90–100). Tests are well-named, single-behavior, and cover the formatter registry's lifecycle end-to-end — null guards, type specificity, LIFO stacking, fall-through chaining, dispose idempotency, fast-path restoration, fault isolation, and acceptance-level scope flow-down. No issues found across the board.

ΔTestGradeBandNotes
newValueFormatterFlowTests.
ValueFormatterScopesFlowDownCorrectly
A90–100No issues found.
newAssertTests.
AddValueFormatter_
AppliesToCollectionItems
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeIsIdempotent
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeRemovesOnlyThatRegistration
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposingAllRegistrationsRestoresFastPath
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ChainsWithTypedOverload
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ReceivesNextDelegate
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Factory_
NullFactory_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
NullValuesAlwaysRenderedAsNullLiteral
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksFallThroughInOrder
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksNewestFirst
A90–100No issues found.
newAssertTests.
AddValueFormatter_
ThrowingFormatterDoesNotDerailRendering
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
CustomizesFailureMessage
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
DoesNotMatchUnrelatedTypes
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
FormatterRemovedAfterDispose
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
NullFormatter_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
ReturningNullFallsThroughToBuiltIn
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 286.5 AIC · ⌖ 13.4 AIC · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 49d5c1b into mainJun 16, 2026
33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/add-value-formatter-9089 branch June 16, 2026 08:00
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 17, 2026
…#9176`, `#9177` (#9207)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Public API for registering custom value formatters used by assertion failure messages

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Assert.AddValueFormatter for customizing assertion failure rendering - #9148

Merged
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089
Jun 16, 2026
Merged

Add Assert.AddValueFormatter for customizing assertion failure rendering#9148
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#9089

Adds two public Assert.AddValueFormatter overloads that let users register custom formatters used by the structured assertion failure message renderer introduced by RFC-012 (#8964):

  • AddValueFormatter<T>(Func<T, string?>) — typed sugar. Return null to fall through to the next formatter / built-in renderer.
  • AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>) — chain-of-responsibility factory that receives the next formatter.

Both overloads return an IDisposable whose disposal removes the registration. Registrations are anchored on AsyncLocal<T>, so the scope follows the test context (per-test, per-class, per-assembly), and they stack newest-first so the most recently registered formatter wins.

Example

[TestMethod]publicvoidDateTimeFailureShowsKind(){usingvar_=Assert.AddValueFormatter<DateTime>(dt =>$""{dt:O} [{dt.Kind}]"");Assert.AreEqual(d1,d2);// failure message now annotates DateTime.Kind}

Implementation

  • New AssertionValueFormatterRegistry (internal, AsyncLocal-backed) holds the chain. Nodes are append-only with a mutable IsRemoved flag so Dispose works for any position in the stack (including out-of-order disposal).
  • AssertionValueRenderer.RenderValue consults the registry ahead of the built-in switch, and only when at least one formatter is registered (zero-allocation fast-path for the common no-formatter case).
  • null is always rendered as ""null"" and is never exposed to user formatters, to avoid spurious NullReferenceExceptions inside user code.
  • User-formatter exceptions are caught and annotated so the underlying assertion failure remains useful.
  • New API is marked [Experimental(""MSTESTEXP"")] consistent with Assert.Scope().

Tests

15 new AssertTests.AddValueFormatter tests cover:

  • ArgumentNullException on both overloads
  • End-to-end assertion failure-message customization
  • Disposal removes the registration (including out-of-order disposal)
  • Idempotent Dispose
  • Newest-first stacking + fall-through ordering
  • Null values bypass the registry
  • Non-matching types bypass the typed overload
  • Collection items get per-item formatting
  • The factory overload composes with the typed overload
  • Throwing formatters do not derail rendering

Verified passing on net9.0, net8.0, and net48.

Out of scope (per the issue)

  • No discovery via attributes/interfaces (deferred).
  • No built-in defaults changed.
  • No equality customization (existing IEqualityComparer<T> overloads cover that).

…ing (#9089)
Adds two public `Assert.AddValueFormatter` overloads that let users register
custom formatters used by the structured assertion failure message renderer:
* `AddValueFormatter<T>(Func<T, string?>)` -- typed sugar.
* `AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>)`
-- chain-of-responsibility factory receiving the next formatter.
Both overloads return an `IDisposable` that removes the registration.
Registrations are anchored on `AsyncLocal<T>` so the scope follows the test
context (per-test, per-class, per-assembly), and they stack newest-first
with null-from-typed-formatter meaning "fall through". Null values continue to
render as `null` and are not exposed to user formatters.
Internally adds `AssertionValueFormatterRegistry` (the AsyncLocal-backed chain)
and wires it into `AssertionValueRenderer.RenderValue` ahead of the built-in
switch so all assertions that surface values through structured messages pick it
up automatically. User-formatter exceptions are caught and annotated to keep the
underlying assertion failure useful.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 11:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental public API to MSTest (Assert.AddValueFormatter) to let users customize how values are rendered in the structured assertion failure messages (via an AsyncLocal-scoped, newest-first formatter stack), and updates the central renderer to consult those formatters before the built-in formatting logic.

Changes:

  • Introduces Assert.AddValueFormatter<T>(Func<T, string?>) and Assert.AddValueFormatter(Func<Func<object?,string>, Func<object?,string>>) returning IDisposable for scoped registration/removal.
  • Adds an internal AssertionValueFormatterRegistry (AsyncLocal-backed) and wires it into AssertionValueRenderer.RenderValue.
  • Adds unit tests covering registration, stacking, disposal semantics, null bypass, and failure-message customization.
Show a summary per file
FileDescription
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AddValueFormatter.csNew unit tests for both overloads, ordering, disposal, and rendering behavior.
src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txtDeclares the new experimental public API signatures.
src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.csConsults the formatter registry before built-in rendering (with a no-formatters fast path).
src/TestFramework/TestFramework/Assertions/AssertionValueFormatterRegistry.csNew internal AsyncLocal registry that composes formatter factories and supports removal via disposal.
src/TestFramework/TestFramework/Assertions/Assert.AddValueFormatter.csImplements the two new public overloads and documents scoping/stacking semantics.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

- Move chain construction inside the try/catch so a throwing user factory
falls back to the built-in renderer instead of escaping.
- Mark Node.IsRemoved volatile so out-of-flow disposal is reliably observed
across threads.
- Inline the chain local to drop the redundant assignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

Prove, against a real MSTest process, that Assert.AddValueFormatter
registrations flow down the AssemblyInitialize -> ClassInitialize ->
TestMethod lifecycle:
- an assembly-scoped formatter is visible in tests,
- a class-scoped formatter wins over and layers on top of the assembly one,
- a test-local registration applies within its test and does not leak into
sibling tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 12:44

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

…cation docs
Addresses PR review feedback:
- AssertionValueFormatterRegistry now prunes leading removed nodes and
resets the AsyncLocal head, so HasFormatters returns to false once every
registration in the flow is disposed (restoring the zero-allocation
fast-path instead of traversing removed nodes on every subsequent render).
- Document that the factory overload's delegate is the chain builder and is
invoked on every render while active, not once at registration time.
- Add a unit test asserting the fast-path is restored after disposing all
registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

The Windows Debug failure was an unrelated, intermittent infra flake in MSBuildTests_KnownExtensionRegistration.Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered: 2 of the 4 net10.0 matrix entries threw Sequence contains no matching element because the _GenerateSelfRegisteredExtensions target was absent from the captured binlog, while the other 2 net10.0 entries (and all other TFMs) passed. This PR only touches Assert code in TestFramework and cannot affect MSBuild extension registration. Pushed an empty commit to re-run CI.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9148

17 test methods graded across 2 files; all 17 earn A (90–100). Tests are well-named, single-behavior, and cover the formatter registry's lifecycle end-to-end — null guards, type specificity, LIFO stacking, fall-through chaining, dispose idempotency, fast-path restoration, fault isolation, and acceptance-level scope flow-down. No issues found across the board.

ΔTestGradeBandNotes
newValueFormatterFlowTests.
ValueFormatterScopesFlowDownCorrectly
A90–100No issues found.
newAssertTests.
AddValueFormatter_
AppliesToCollectionItems
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeIsIdempotent
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeRemovesOnlyThatRegistration
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposingAllRegistrationsRestoresFastPath
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ChainsWithTypedOverload
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ReceivesNextDelegate
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Factory_
NullFactory_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
NullValuesAlwaysRenderedAsNullLiteral
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksFallThroughInOrder
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksNewestFirst
A90–100No issues found.
newAssertTests.
AddValueFormatter_
ThrowingFormatterDoesNotDerailRendering
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
CustomizesFailureMessage
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
DoesNotMatchUnrelatedTypes
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
FormatterRemovedAfterDispose
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
NullFormatter_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
ReturningNullFallsThroughToBuiltIn
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 286.5 AIC · ⌖ 13.4 AIC · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 49d5c1b into mainJun 16, 2026
33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/add-value-formatter-9089 branch June 16, 2026 08:00
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 17, 2026
…#9176`, `#9177` (#9207)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Public API for registering custom value formatters used by assertion failure messages

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add Assert.AddValueFormatter for customizing assertion failure rendering - #9148

Merged
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089
Jun 16, 2026
Merged

Add Assert.AddValueFormatter for customizing assertion failure rendering#9148
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#9089

Adds two public Assert.AddValueFormatter overloads that let users register custom formatters used by the structured assertion failure message renderer introduced by RFC-012 (#8964):

  • AddValueFormatter<T>(Func<T, string?>) — typed sugar. Return null to fall through to the next formatter / built-in renderer.
  • AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>) — chain-of-responsibility factory that receives the next formatter.

Both overloads return an IDisposable whose disposal removes the registration. Registrations are anchored on AsyncLocal<T>, so the scope follows the test context (per-test, per-class, per-assembly), and they stack newest-first so the most recently registered formatter wins.

Example

[TestMethod]publicvoidDateTimeFailureShowsKind(){usingvar_=Assert.AddValueFormatter<DateTime>(dt =>$""{dt:O} [{dt.Kind}]"");Assert.AreEqual(d1,d2);// failure message now annotates DateTime.Kind}

Implementation

  • New AssertionValueFormatterRegistry (internal, AsyncLocal-backed) holds the chain. Nodes are append-only with a mutable IsRemoved flag so Dispose works for any position in the stack (including out-of-order disposal).
  • AssertionValueRenderer.RenderValue consults the registry ahead of the built-in switch, and only when at least one formatter is registered (zero-allocation fast-path for the common no-formatter case).
  • null is always rendered as ""null"" and is never exposed to user formatters, to avoid spurious NullReferenceExceptions inside user code.
  • User-formatter exceptions are caught and annotated so the underlying assertion failure remains useful.
  • New API is marked [Experimental(""MSTESTEXP"")] consistent with Assert.Scope().

Tests

15 new AssertTests.AddValueFormatter tests cover:

  • ArgumentNullException on both overloads
  • End-to-end assertion failure-message customization
  • Disposal removes the registration (including out-of-order disposal)
  • Idempotent Dispose
  • Newest-first stacking + fall-through ordering
  • Null values bypass the registry
  • Non-matching types bypass the typed overload
  • Collection items get per-item formatting
  • The factory overload composes with the typed overload
  • Throwing formatters do not derail rendering

Verified passing on net9.0, net8.0, and net48.

Out of scope (per the issue)

  • No discovery via attributes/interfaces (deferred).
  • No built-in defaults changed.
  • No equality customization (existing IEqualityComparer<T> overloads cover that).

…ing (#9089)
Adds two public `Assert.AddValueFormatter` overloads that let users register
custom formatters used by the structured assertion failure message renderer:
* `AddValueFormatter<T>(Func<T, string?>)` -- typed sugar.
* `AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>)`
-- chain-of-responsibility factory receiving the next formatter.
Both overloads return an `IDisposable` that removes the registration.
Registrations are anchored on `AsyncLocal<T>` so the scope follows the test
context (per-test, per-class, per-assembly), and they stack newest-first
with null-from-typed-formatter meaning "fall through". Null values continue to
render as `null` and are not exposed to user formatters.
Internally adds `AssertionValueFormatterRegistry` (the AsyncLocal-backed chain)
and wires it into `AssertionValueRenderer.RenderValue` ahead of the built-in
switch so all assertions that surface values through structured messages pick it
up automatically. User-formatter exceptions are caught and annotated to keep the
underlying assertion failure useful.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 11:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental public API to MSTest (Assert.AddValueFormatter) to let users customize how values are rendered in the structured assertion failure messages (via an AsyncLocal-scoped, newest-first formatter stack), and updates the central renderer to consult those formatters before the built-in formatting logic.

Changes:

  • Introduces Assert.AddValueFormatter<T>(Func<T, string?>) and Assert.AddValueFormatter(Func<Func<object?,string>, Func<object?,string>>) returning IDisposable for scoped registration/removal.
  • Adds an internal AssertionValueFormatterRegistry (AsyncLocal-backed) and wires it into AssertionValueRenderer.RenderValue.
  • Adds unit tests covering registration, stacking, disposal semantics, null bypass, and failure-message customization.
Show a summary per file
FileDescription
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AddValueFormatter.csNew unit tests for both overloads, ordering, disposal, and rendering behavior.
src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txtDeclares the new experimental public API signatures.
src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.csConsults the formatter registry before built-in rendering (with a no-formatters fast path).
src/TestFramework/TestFramework/Assertions/AssertionValueFormatterRegistry.csNew internal AsyncLocal registry that composes formatter factories and supports removal via disposal.
src/TestFramework/TestFramework/Assertions/Assert.AddValueFormatter.csImplements the two new public overloads and documents scoping/stacking semantics.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

- Move chain construction inside the try/catch so a throwing user factory
falls back to the built-in renderer instead of escaping.
- Mark Node.IsRemoved volatile so out-of-flow disposal is reliably observed
across threads.
- Inline the chain local to drop the redundant assignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

Prove, against a real MSTest process, that Assert.AddValueFormatter
registrations flow down the AssemblyInitialize -> ClassInitialize ->
TestMethod lifecycle:
- an assembly-scoped formatter is visible in tests,
- a class-scoped formatter wins over and layers on top of the assembly one,
- a test-local registration applies within its test and does not leak into
sibling tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 12:44

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

…cation docs
Addresses PR review feedback:
- AssertionValueFormatterRegistry now prunes leading removed nodes and
resets the AsyncLocal head, so HasFormatters returns to false once every
registration in the flow is disposed (restoring the zero-allocation
fast-path instead of traversing removed nodes on every subsequent render).
- Document that the factory overload's delegate is the chain builder and is
invoked on every render while active, not once at registration time.
- Add a unit test asserting the fast-path is restored after disposing all
registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

The Windows Debug failure was an unrelated, intermittent infra flake in MSBuildTests_KnownExtensionRegistration.Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered: 2 of the 4 net10.0 matrix entries threw Sequence contains no matching element because the _GenerateSelfRegisteredExtensions target was absent from the captured binlog, while the other 2 net10.0 entries (and all other TFMs) passed. This PR only touches Assert code in TestFramework and cannot affect MSBuild extension registration. Pushed an empty commit to re-run CI.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9148

17 test methods graded across 2 files; all 17 earn A (90–100). Tests are well-named, single-behavior, and cover the formatter registry's lifecycle end-to-end — null guards, type specificity, LIFO stacking, fall-through chaining, dispose idempotency, fast-path restoration, fault isolation, and acceptance-level scope flow-down. No issues found across the board.

ΔTestGradeBandNotes
newValueFormatterFlowTests.
ValueFormatterScopesFlowDownCorrectly
A90–100No issues found.
newAssertTests.
AddValueFormatter_
AppliesToCollectionItems
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeIsIdempotent
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeRemovesOnlyThatRegistration
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposingAllRegistrationsRestoresFastPath
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ChainsWithTypedOverload
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ReceivesNextDelegate
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Factory_
NullFactory_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
NullValuesAlwaysRenderedAsNullLiteral
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksFallThroughInOrder
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksNewestFirst
A90–100No issues found.
newAssertTests.
AddValueFormatter_
ThrowingFormatterDoesNotDerailRendering
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
CustomizesFailureMessage
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
DoesNotMatchUnrelatedTypes
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
FormatterRemovedAfterDispose
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
NullFormatter_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
ReturningNullFallsThroughToBuiltIn
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 286.5 AIC · ⌖ 13.4 AIC · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 49d5c1b into mainJun 16, 2026
33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/add-value-formatter-9089 branch June 16, 2026 08:00
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 17, 2026
…#9176`, `#9177` (#9207)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Public API for registering custom value formatters used by assertion failure messages

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Assert.AddValueFormatter for customizing assertion failure rendering - #9148

Merged
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089
Jun 16, 2026
Merged

Add Assert.AddValueFormatter for customizing assertion failure rendering#9148
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#9089

Adds two public Assert.AddValueFormatter overloads that let users register custom formatters used by the structured assertion failure message renderer introduced by RFC-012 (#8964):

  • AddValueFormatter<T>(Func<T, string?>) — typed sugar. Return null to fall through to the next formatter / built-in renderer.
  • AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>) — chain-of-responsibility factory that receives the next formatter.

Both overloads return an IDisposable whose disposal removes the registration. Registrations are anchored on AsyncLocal<T>, so the scope follows the test context (per-test, per-class, per-assembly), and they stack newest-first so the most recently registered formatter wins.

Example

[TestMethod]publicvoidDateTimeFailureShowsKind(){usingvar_=Assert.AddValueFormatter<DateTime>(dt =>$""{dt:O} [{dt.Kind}]"");Assert.AreEqual(d1,d2);// failure message now annotates DateTime.Kind}

Implementation

  • New AssertionValueFormatterRegistry (internal, AsyncLocal-backed) holds the chain. Nodes are append-only with a mutable IsRemoved flag so Dispose works for any position in the stack (including out-of-order disposal).
  • AssertionValueRenderer.RenderValue consults the registry ahead of the built-in switch, and only when at least one formatter is registered (zero-allocation fast-path for the common no-formatter case).
  • null is always rendered as ""null"" and is never exposed to user formatters, to avoid spurious NullReferenceExceptions inside user code.
  • User-formatter exceptions are caught and annotated so the underlying assertion failure remains useful.
  • New API is marked [Experimental(""MSTESTEXP"")] consistent with Assert.Scope().

Tests

15 new AssertTests.AddValueFormatter tests cover:

  • ArgumentNullException on both overloads
  • End-to-end assertion failure-message customization
  • Disposal removes the registration (including out-of-order disposal)
  • Idempotent Dispose
  • Newest-first stacking + fall-through ordering
  • Null values bypass the registry
  • Non-matching types bypass the typed overload
  • Collection items get per-item formatting
  • The factory overload composes with the typed overload
  • Throwing formatters do not derail rendering

Verified passing on net9.0, net8.0, and net48.

Out of scope (per the issue)

  • No discovery via attributes/interfaces (deferred).
  • No built-in defaults changed.
  • No equality customization (existing IEqualityComparer<T> overloads cover that).

…ing (#9089)
Adds two public `Assert.AddValueFormatter` overloads that let users register
custom formatters used by the structured assertion failure message renderer:
* `AddValueFormatter<T>(Func<T, string?>)` -- typed sugar.
* `AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>)`
-- chain-of-responsibility factory receiving the next formatter.
Both overloads return an `IDisposable` that removes the registration.
Registrations are anchored on `AsyncLocal<T>` so the scope follows the test
context (per-test, per-class, per-assembly), and they stack newest-first
with null-from-typed-formatter meaning "fall through". Null values continue to
render as `null` and are not exposed to user formatters.
Internally adds `AssertionValueFormatterRegistry` (the AsyncLocal-backed chain)
and wires it into `AssertionValueRenderer.RenderValue` ahead of the built-in
switch so all assertions that surface values through structured messages pick it
up automatically. User-formatter exceptions are caught and annotated to keep the
underlying assertion failure useful.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 11:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental public API to MSTest (Assert.AddValueFormatter) to let users customize how values are rendered in the structured assertion failure messages (via an AsyncLocal-scoped, newest-first formatter stack), and updates the central renderer to consult those formatters before the built-in formatting logic.

Changes:

  • Introduces Assert.AddValueFormatter<T>(Func<T, string?>) and Assert.AddValueFormatter(Func<Func<object?,string>, Func<object?,string>>) returning IDisposable for scoped registration/removal.
  • Adds an internal AssertionValueFormatterRegistry (AsyncLocal-backed) and wires it into AssertionValueRenderer.RenderValue.
  • Adds unit tests covering registration, stacking, disposal semantics, null bypass, and failure-message customization.
Show a summary per file
FileDescription
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AddValueFormatter.csNew unit tests for both overloads, ordering, disposal, and rendering behavior.
src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txtDeclares the new experimental public API signatures.
src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.csConsults the formatter registry before built-in rendering (with a no-formatters fast path).
src/TestFramework/TestFramework/Assertions/AssertionValueFormatterRegistry.csNew internal AsyncLocal registry that composes formatter factories and supports removal via disposal.
src/TestFramework/TestFramework/Assertions/Assert.AddValueFormatter.csImplements the two new public overloads and documents scoping/stacking semantics.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

- Move chain construction inside the try/catch so a throwing user factory
falls back to the built-in renderer instead of escaping.
- Mark Node.IsRemoved volatile so out-of-flow disposal is reliably observed
across threads.
- Inline the chain local to drop the redundant assignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

Prove, against a real MSTest process, that Assert.AddValueFormatter
registrations flow down the AssemblyInitialize -> ClassInitialize ->
TestMethod lifecycle:
- an assembly-scoped formatter is visible in tests,
- a class-scoped formatter wins over and layers on top of the assembly one,
- a test-local registration applies within its test and does not leak into
sibling tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 12:44

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

…cation docs
Addresses PR review feedback:
- AssertionValueFormatterRegistry now prunes leading removed nodes and
resets the AsyncLocal head, so HasFormatters returns to false once every
registration in the flow is disposed (restoring the zero-allocation
fast-path instead of traversing removed nodes on every subsequent render).
- Document that the factory overload's delegate is the chain builder and is
invoked on every render while active, not once at registration time.
- Add a unit test asserting the fast-path is restored after disposing all
registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

The Windows Debug failure was an unrelated, intermittent infra flake in MSBuildTests_KnownExtensionRegistration.Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered: 2 of the 4 net10.0 matrix entries threw Sequence contains no matching element because the _GenerateSelfRegisteredExtensions target was absent from the captured binlog, while the other 2 net10.0 entries (and all other TFMs) passed. This PR only touches Assert code in TestFramework and cannot affect MSBuild extension registration. Pushed an empty commit to re-run CI.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9148

17 test methods graded across 2 files; all 17 earn A (90–100). Tests are well-named, single-behavior, and cover the formatter registry's lifecycle end-to-end — null guards, type specificity, LIFO stacking, fall-through chaining, dispose idempotency, fast-path restoration, fault isolation, and acceptance-level scope flow-down. No issues found across the board.

ΔTestGradeBandNotes
newValueFormatterFlowTests.
ValueFormatterScopesFlowDownCorrectly
A90–100No issues found.
newAssertTests.
AddValueFormatter_
AppliesToCollectionItems
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeIsIdempotent
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeRemovesOnlyThatRegistration
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposingAllRegistrationsRestoresFastPath
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ChainsWithTypedOverload
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ReceivesNextDelegate
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Factory_
NullFactory_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
NullValuesAlwaysRenderedAsNullLiteral
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksFallThroughInOrder
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksNewestFirst
A90–100No issues found.
newAssertTests.
AddValueFormatter_
ThrowingFormatterDoesNotDerailRendering
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
CustomizesFailureMessage
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
DoesNotMatchUnrelatedTypes
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
FormatterRemovedAfterDispose
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
NullFormatter_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
ReturningNullFallsThroughToBuiltIn
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 286.5 AIC · ⌖ 13.4 AIC · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 49d5c1b into mainJun 16, 2026
33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/add-value-formatter-9089 branch June 16, 2026 08:00
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 17, 2026
…#9176`, `#9177` (#9207)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Public API for registering custom value formatters used by assertion failure messages

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Assert.AddValueFormatter for customizing assertion failure rendering - #9148

Merged
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089
Jun 16, 2026
Merged

Add Assert.AddValueFormatter for customizing assertion failure rendering#9148
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#9089

Adds two public Assert.AddValueFormatter overloads that let users register custom formatters used by the structured assertion failure message renderer introduced by RFC-012 (#8964):

  • AddValueFormatter<T>(Func<T, string?>) — typed sugar. Return null to fall through to the next formatter / built-in renderer.
  • AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>) — chain-of-responsibility factory that receives the next formatter.

Both overloads return an IDisposable whose disposal removes the registration. Registrations are anchored on AsyncLocal<T>, so the scope follows the test context (per-test, per-class, per-assembly), and they stack newest-first so the most recently registered formatter wins.

Example

[TestMethod]publicvoidDateTimeFailureShowsKind(){usingvar_=Assert.AddValueFormatter<DateTime>(dt =>$""{dt:O} [{dt.Kind}]"");Assert.AreEqual(d1,d2);// failure message now annotates DateTime.Kind}

Implementation

  • New AssertionValueFormatterRegistry (internal, AsyncLocal-backed) holds the chain. Nodes are append-only with a mutable IsRemoved flag so Dispose works for any position in the stack (including out-of-order disposal).
  • AssertionValueRenderer.RenderValue consults the registry ahead of the built-in switch, and only when at least one formatter is registered (zero-allocation fast-path for the common no-formatter case).
  • null is always rendered as ""null"" and is never exposed to user formatters, to avoid spurious NullReferenceExceptions inside user code.
  • User-formatter exceptions are caught and annotated so the underlying assertion failure remains useful.
  • New API is marked [Experimental(""MSTESTEXP"")] consistent with Assert.Scope().

Tests

15 new AssertTests.AddValueFormatter tests cover:

  • ArgumentNullException on both overloads
  • End-to-end assertion failure-message customization
  • Disposal removes the registration (including out-of-order disposal)
  • Idempotent Dispose
  • Newest-first stacking + fall-through ordering
  • Null values bypass the registry
  • Non-matching types bypass the typed overload
  • Collection items get per-item formatting
  • The factory overload composes with the typed overload
  • Throwing formatters do not derail rendering

Verified passing on net9.0, net8.0, and net48.

Out of scope (per the issue)

  • No discovery via attributes/interfaces (deferred).
  • No built-in defaults changed.
  • No equality customization (existing IEqualityComparer<T> overloads cover that).

…ing (#9089)
Adds two public `Assert.AddValueFormatter` overloads that let users register
custom formatters used by the structured assertion failure message renderer:
* `AddValueFormatter<T>(Func<T, string?>)` -- typed sugar.
* `AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>)`
-- chain-of-responsibility factory receiving the next formatter.
Both overloads return an `IDisposable` that removes the registration.
Registrations are anchored on `AsyncLocal<T>` so the scope follows the test
context (per-test, per-class, per-assembly), and they stack newest-first
with null-from-typed-formatter meaning "fall through". Null values continue to
render as `null` and are not exposed to user formatters.
Internally adds `AssertionValueFormatterRegistry` (the AsyncLocal-backed chain)
and wires it into `AssertionValueRenderer.RenderValue` ahead of the built-in
switch so all assertions that surface values through structured messages pick it
up automatically. User-formatter exceptions are caught and annotated to keep the
underlying assertion failure useful.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 11:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental public API to MSTest (Assert.AddValueFormatter) to let users customize how values are rendered in the structured assertion failure messages (via an AsyncLocal-scoped, newest-first formatter stack), and updates the central renderer to consult those formatters before the built-in formatting logic.

Changes:

  • Introduces Assert.AddValueFormatter<T>(Func<T, string?>) and Assert.AddValueFormatter(Func<Func<object?,string>, Func<object?,string>>) returning IDisposable for scoped registration/removal.
  • Adds an internal AssertionValueFormatterRegistry (AsyncLocal-backed) and wires it into AssertionValueRenderer.RenderValue.
  • Adds unit tests covering registration, stacking, disposal semantics, null bypass, and failure-message customization.
Show a summary per file
FileDescription
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AddValueFormatter.csNew unit tests for both overloads, ordering, disposal, and rendering behavior.
src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txtDeclares the new experimental public API signatures.
src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.csConsults the formatter registry before built-in rendering (with a no-formatters fast path).
src/TestFramework/TestFramework/Assertions/AssertionValueFormatterRegistry.csNew internal AsyncLocal registry that composes formatter factories and supports removal via disposal.
src/TestFramework/TestFramework/Assertions/Assert.AddValueFormatter.csImplements the two new public overloads and documents scoping/stacking semantics.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

- Move chain construction inside the try/catch so a throwing user factory
falls back to the built-in renderer instead of escaping.
- Mark Node.IsRemoved volatile so out-of-flow disposal is reliably observed
across threads.
- Inline the chain local to drop the redundant assignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

Prove, against a real MSTest process, that Assert.AddValueFormatter
registrations flow down the AssemblyInitialize -> ClassInitialize ->
TestMethod lifecycle:
- an assembly-scoped formatter is visible in tests,
- a class-scoped formatter wins over and layers on top of the assembly one,
- a test-local registration applies within its test and does not leak into
sibling tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 12:44

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

…cation docs
Addresses PR review feedback:
- AssertionValueFormatterRegistry now prunes leading removed nodes and
resets the AsyncLocal head, so HasFormatters returns to false once every
registration in the flow is disposed (restoring the zero-allocation
fast-path instead of traversing removed nodes on every subsequent render).
- Document that the factory overload's delegate is the chain builder and is
invoked on every render while active, not once at registration time.
- Add a unit test asserting the fast-path is restored after disposing all
registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

The Windows Debug failure was an unrelated, intermittent infra flake in MSBuildTests_KnownExtensionRegistration.Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered: 2 of the 4 net10.0 matrix entries threw Sequence contains no matching element because the _GenerateSelfRegisteredExtensions target was absent from the captured binlog, while the other 2 net10.0 entries (and all other TFMs) passed. This PR only touches Assert code in TestFramework and cannot affect MSBuild extension registration. Pushed an empty commit to re-run CI.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9148

17 test methods graded across 2 files; all 17 earn A (90–100). Tests are well-named, single-behavior, and cover the formatter registry's lifecycle end-to-end — null guards, type specificity, LIFO stacking, fall-through chaining, dispose idempotency, fast-path restoration, fault isolation, and acceptance-level scope flow-down. No issues found across the board.

ΔTestGradeBandNotes
newValueFormatterFlowTests.
ValueFormatterScopesFlowDownCorrectly
A90–100No issues found.
newAssertTests.
AddValueFormatter_
AppliesToCollectionItems
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeIsIdempotent
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeRemovesOnlyThatRegistration
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposingAllRegistrationsRestoresFastPath
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ChainsWithTypedOverload
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ReceivesNextDelegate
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Factory_
NullFactory_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
NullValuesAlwaysRenderedAsNullLiteral
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksFallThroughInOrder
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksNewestFirst
A90–100No issues found.
newAssertTests.
AddValueFormatter_
ThrowingFormatterDoesNotDerailRendering
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
CustomizesFailureMessage
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
DoesNotMatchUnrelatedTypes
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
FormatterRemovedAfterDispose
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
NullFormatter_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
ReturningNullFallsThroughToBuiltIn
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 286.5 AIC · ⌖ 13.4 AIC · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 49d5c1b into mainJun 16, 2026
33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/add-value-formatter-9089 branch June 16, 2026 08:00
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 17, 2026
…#9176`, `#9177` (#9207)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Public API for registering custom value formatters used by assertion failure messages

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add Assert.AddValueFormatter for customizing assertion failure rendering - #9148

Merged
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089
Jun 16, 2026
Merged

Add Assert.AddValueFormatter for customizing assertion failure rendering#9148
Amaury Levé (Evangelink) merged 5 commits into
mainfrom
dev/amauryleve/add-value-formatter-9089

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#9089

Adds two public Assert.AddValueFormatter overloads that let users register custom formatters used by the structured assertion failure message renderer introduced by RFC-012 (#8964):

  • AddValueFormatter<T>(Func<T, string?>) — typed sugar. Return null to fall through to the next formatter / built-in renderer.
  • AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>) — chain-of-responsibility factory that receives the next formatter.

Both overloads return an IDisposable whose disposal removes the registration. Registrations are anchored on AsyncLocal<T>, so the scope follows the test context (per-test, per-class, per-assembly), and they stack newest-first so the most recently registered formatter wins.

Example

[TestMethod]publicvoidDateTimeFailureShowsKind(){usingvar_=Assert.AddValueFormatter<DateTime>(dt =>$""{dt:O} [{dt.Kind}]"");Assert.AreEqual(d1,d2);// failure message now annotates DateTime.Kind}

Implementation

  • New AssertionValueFormatterRegistry (internal, AsyncLocal-backed) holds the chain. Nodes are append-only with a mutable IsRemoved flag so Dispose works for any position in the stack (including out-of-order disposal).
  • AssertionValueRenderer.RenderValue consults the registry ahead of the built-in switch, and only when at least one formatter is registered (zero-allocation fast-path for the common no-formatter case).
  • null is always rendered as ""null"" and is never exposed to user formatters, to avoid spurious NullReferenceExceptions inside user code.
  • User-formatter exceptions are caught and annotated so the underlying assertion failure remains useful.
  • New API is marked [Experimental(""MSTESTEXP"")] consistent with Assert.Scope().

Tests

15 new AssertTests.AddValueFormatter tests cover:

  • ArgumentNullException on both overloads
  • End-to-end assertion failure-message customization
  • Disposal removes the registration (including out-of-order disposal)
  • Idempotent Dispose
  • Newest-first stacking + fall-through ordering
  • Null values bypass the registry
  • Non-matching types bypass the typed overload
  • Collection items get per-item formatting
  • The factory overload composes with the typed overload
  • Throwing formatters do not derail rendering

Verified passing on net9.0, net8.0, and net48.

Out of scope (per the issue)

  • No discovery via attributes/interfaces (deferred).
  • No built-in defaults changed.
  • No equality customization (existing IEqualityComparer<T> overloads cover that).

…ing (#9089)
Adds two public `Assert.AddValueFormatter` overloads that let users register
custom formatters used by the structured assertion failure message renderer:
* `AddValueFormatter<T>(Func<T, string?>)` -- typed sugar.
* `AddValueFormatter(Func<Func<object?, string>, Func<object?, string>>)`
-- chain-of-responsibility factory receiving the next formatter.
Both overloads return an `IDisposable` that removes the registration.
Registrations are anchored on `AsyncLocal<T>` so the scope follows the test
context (per-test, per-class, per-assembly), and they stack newest-first
with null-from-typed-formatter meaning "fall through". Null values continue to
render as `null` and are not exposed to user formatters.
Internally adds `AssertionValueFormatterRegistry` (the AsyncLocal-backed chain)
and wires it into `AssertionValueRenderer.RenderValue` ahead of the built-in
switch so all assertions that surface values through structured messages pick it
up automatically. User-formatter exceptions are caught and annotated to keep the
underlying assertion failure useful.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 11:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental public API to MSTest (Assert.AddValueFormatter) to let users customize how values are rendered in the structured assertion failure messages (via an AsyncLocal-scoped, newest-first formatter stack), and updates the central renderer to consult those formatters before the built-in formatting logic.

Changes:

  • Introduces Assert.AddValueFormatter<T>(Func<T, string?>) and Assert.AddValueFormatter(Func<Func<object?,string>, Func<object?,string>>) returning IDisposable for scoped registration/removal.
  • Adds an internal AssertionValueFormatterRegistry (AsyncLocal-backed) and wires it into AssertionValueRenderer.RenderValue.
  • Adds unit tests covering registration, stacking, disposal semantics, null bypass, and failure-message customization.
Show a summary per file
FileDescription
test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AddValueFormatter.csNew unit tests for both overloads, ordering, disposal, and rendering behavior.
src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txtDeclares the new experimental public API signatures.
src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.csConsults the formatter registry before built-in rendering (with a no-formatters fast path).
src/TestFramework/TestFramework/Assertions/AssertionValueFormatterRegistry.csNew internal AsyncLocal registry that composes formatter factories and supports removal via disposal.
src/TestFramework/TestFramework/Assertions/Assert.AddValueFormatter.csImplements the two new public overloads and documents scoping/stacking semantics.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

- Move chain construction inside the try/catch so a throwing user factory
falls back to the built-in renderer instead of escaping.
- Mark Node.IsRemoved volatile so out-of-flow disposal is reliably observed
across threads.
- Inline the chain local to drop the redundant assignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

This comment has been minimized.

Prove, against a real MSTest process, that Assert.AddValueFormatter
registrations flow down the AssemblyInitialize -> ClassInitialize ->
TestMethod lifecycle:
- an assembly-scoped formatter is visible in tests,
- a class-scoped formatter wins over and layers on top of the assembly one,
- a test-local registration applies within its test and does not leak into
sibling tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 12:44

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

@Evangelink

This comment has been minimized.

…cation docs
Addresses PR review feedback:
- AssertionValueFormatterRegistry now prunes leading removed nodes and
resets the AsyncLocal head, so HasFormatters returns to false once every
registration in the flow is disposed (restoring the zero-allocation
fast-path instead of traversing removed nodes on every subsequent render).
- Document that the factory overload's delegate is the chain builder and is
invoked on every render while active, not once at registration time.
- Add a unit test asserting the fast-path is restored after disposing all
registrations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

The Windows Debug failure was an unrelated, intermittent infra flake in MSBuildTests_KnownExtensionRegistration.Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered: 2 of the 4 net10.0 matrix entries threw Sequence contains no matching element because the _GenerateSelfRegisteredExtensions target was absent from the captured binlog, while the other 2 net10.0 entries (and all other TFMs) passed. This PR only touches Assert code in TestFramework and cannot affect MSBuild extension registration. Pushed an empty commit to re-run CI.

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9148

17 test methods graded across 2 files; all 17 earn A (90–100). Tests are well-named, single-behavior, and cover the formatter registry's lifecycle end-to-end — null guards, type specificity, LIFO stacking, fall-through chaining, dispose idempotency, fast-path restoration, fault isolation, and acceptance-level scope flow-down. No issues found across the board.

ΔTestGradeBandNotes
newValueFormatterFlowTests.
ValueFormatterScopesFlowDownCorrectly
A90–100No issues found.
newAssertTests.
AddValueFormatter_
AppliesToCollectionItems
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeIsIdempotent
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposeRemovesOnlyThatRegistration
A90–100No issues found.
newAssertTests.
AddValueFormatter_
DisposingAllRegistrationsRestoresFastPath
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ChainsWithTypedOverload
A90–100No issues found.
newAssertTests.
AddValueFormatter_
FactoryOverload_
ReceivesNextDelegate
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Factory_
NullFactory_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
NullValuesAlwaysRenderedAsNullLiteral
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksFallThroughInOrder
A90–100No issues found.
newAssertTests.
AddValueFormatter_
StacksNewestFirst
A90–100No issues found.
newAssertTests.
AddValueFormatter_
ThrowingFormatterDoesNotDerailRendering
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
CustomizesFailureMessage
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
DoesNotMatchUnrelatedTypes
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
FormatterRemovedAfterDispose
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
NullFormatter_
ThrowsArgumentNullException
A90–100No issues found.
newAssertTests.
AddValueFormatter_
Typed_
ReturningNullFallsThroughToBuiltIn
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 286.5 AIC · ⌖ 13.4 AIC · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 49d5c1b into mainJun 16, 2026
33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/add-value-formatter-9089 branch June 16, 2026 08:00
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 17, 2026
…#9176`, `#9177` (#9207)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Public API for registering custom value formatters used by assertion failure messages

2 participants

@Evangelink