Skip to content

Use StructuredAssertionMessage for StringAssert and CollectionAssert membership (#9275)#9297

Open
Evangelink wants to merge 6 commits into
mainfrom
dev/structured-messages-legacy-asserts-9275
Open

Use StructuredAssertionMessage for StringAssert and CollectionAssert membership (#9275)#9297
Evangelink wants to merge 6 commits into
mainfrom
dev/structured-messages-legacy-asserts-9275

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes #9275 (Tasks 1, 2, 3, 5).

Migrates the legacy string.Format + FrameworkMessages.*Fail failure path to the modern StructuredAssertionMessage infrastructure so failures expose machine-readable ExpectedText/ActualText, populate exception.Data["assert.expected"|"assert.actual"], and render aligned evidence blocks — matching the diagnostics already produced by the equivalent Assert.* methods.

Changes

  • StringAssert (StringAssert.cs): Contains, StartsWith, EndsWith, Matches, DoesNotMatch.
  • CollectionAssert membership (CollectionAssert.Membership.cs): Contains, DoesNotContain, AllItemsAreNotNull, AllItemsAreUnique (the duplicate item is surfaced for AllItemsAreUnique).
  • Tests: 13 new unit tests asserting ExpectedText/ActualText/Data; updated 6 existing StringAssertTests that checked the old "StringAssert.X failed" text.

Notes

  • All reused summary resources already existed, so no .resx/.xlf regeneration and no public API changes were required.
  • Task 4 (CollectionAssert equality/equivalence: AreEqual/AreNotEqual/AreEquivalent/AreNotEquivalent) is intentionally deferred to a follow-up — it is a larger/riskier change requiring full-collection rendering while preserving index-level mismatch detail across 6 source files.

Validation

  • TestFramework.csproj builds clean across all TFMs (net462/netstandard2.0/net8.0/net9.0), 0 warnings.
  • TestFramework.UnitTests: 1319/1319 passed.

Amaury Levé and others added 3 commits June 20, 2026 11:08
…rloads (#9279)

Each Assert.*InterpolatedStringHandler struct must declare the same ~10
AppendLiteral/AppendFormatted overloads because the C# interpolated string
handler pattern resolves them via ordinary member lookup on each handler
struct (structs cannot inherit them). #9282 centralized the append *logic*
into AssertInterpolatedStringHandlerAppender but the ~23 forwarder blocks
(~580 lines) remained, near-identical, in a single file.

This adds a small source generator (TestFramework.SourceGeneration) that
emits those forwarders for any handler annotated with
[GenerateAssertInterpolatedStringAppendMethods]. The generated public API is
byte-identical to the previous hand-written overloads (verified by the
PublicAPI analyzer passing unchanged across netstandard2.0/net462/net8.0/
net9.0), so there is no public API change. The handler files now only declare
their state, constructor, and ComputeAssertion logic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rload regression tests

- The generator now emits the containing type with its real accessibility/modifiers
  (e.g. 'public sealed partial class Assert') derived from the symbol, instead of a
  bare 'partial class'. (Partial declarations may omit modifiers, so the build was
  already valid, but matching them is clearer and more defensive.)
- Added AssertInterpolatedStringHandlerGeneratedOverloadsTests covering each
  generated AppendLiteral/AppendFormatted overload case (literal, generic value,
  format, alignment, alignment+format, string, object, and ReadOnlySpan<char>)
  end-to-end through real failing interpolated assertions, plus cross-handler cases
  (IsFalse, AreEqual generic+nullable-literal, IsNull) so a handler that lost its
  marker attribute fails the suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…membership (#9275)

Migrate the legacy string.Format + FrameworkMessages.*Fail failure path of StringAssert (Contains, StartsWith, EndsWith, Matches, DoesNotMatch) and the CollectionAssert membership methods (Contains, DoesNotContain, AllItemsAreNotNull, AllItemsAreUnique) to the modern StructuredAssertionMessage infrastructure. Failures now expose machine-readable ExpectedText/ActualText and exception.Data entries plus aligned evidence blocks, matching the diagnostics already produced by the equivalent Assert.* methods. All reused summary resources already exist, so no .resx/.xlf or public API changes are needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 20, 2026 12:45

Copilot AI 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

This PR modernizes legacy MSTest assertions by routing StringAssert and CollectionAssert membership failures through StructuredAssertionMessage, enabling consistent structured diagnostics (ExpectedText/ActualText and exception.Data["assert.expected"/"assert.actual"]) similar to existing Assert.* APIs. It also introduces an internal source generator to eliminate duplicated interpolated-string-handler AppendLiteral/AppendFormatted boilerplate across Assert.*InterpolatedStringHandler structs.

Changes:

  • Migrated StringAssert substring/regex failure paths to StructuredAssertionMessage, with evidence blocks and populated expected/actual metadata.
  • Migrated CollectionAssert membership failures to StructuredAssertionMessage, including duplicate surfacing for AllItemsAreUnique.
  • Added a new internal source generator project and updated Assert.*InterpolatedStringHandler structs to opt-in via a marker attribute; added regression tests for generated overload sets.
Show a summary per file
File Description
TestFx.slnx Adds the new TestFramework.SourceGeneration project to the solution.
src/TestFramework/TestFramework/TestFramework.csproj References the generator project as an analyzer to emit handler append-method overloads at build time.
src/TestFramework/TestFramework/Assertions/StringAssert.cs Converts failure reporting to structured messages for substring and regex assertions.
src/TestFramework/TestFramework/Assertions/CollectionAssert.Membership.cs Converts membership failure reporting to structured messages and adds evidence blocks.
test/UnitTests/TestFramework.UnitTests/Assertions/StringAssertTests.cs Updates message expectations and adds structured expected/actual/data assertions for StringAssert.*.
test/UnitTests/TestFramework.UnitTests/Assertions/CollectionAssertTests.cs Adds structured message/data assertions for CollectionAssert membership methods.
test/UnitTests/TestFramework.UnitTests/Assertions/AssertInterpolatedStringHandlerGeneratedOverloadsTests.cs Adds regression coverage to ensure generated handler overloads compile/bind and render correctly end-to-end.
src/TestFramework/TestFramework/Assertions/GenerateAssertInterpolatedStringAppendMethodsAttribute.cs Adds an internal marker attribute to opt handler structs into source generation.
src/TestFramework/TestFramework/Assertions/Assert.InterpolatedStringHandlers.AppendMethods.cs Removes the duplicated per-handler append-method implementations in favor of generator output.
src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework/Assertions/Assert.IsEmpty.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework/Assertions/Assert.Count.InterpolatedStringHandlers.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework/Assertions/Assert.ContainsSingle.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs Opts handler structs into generated append-method overloads (including nullable literal configuration for the one handler).
src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.InterpolatedStringHandlers.cs Opts handler structs into generated append-method overloads.
src/TestFramework/TestFramework.SourceGeneration/TestFramework.SourceGeneration.csproj Introduces the internal build-time generator project (not packed/shipped).
src/TestFramework/TestFramework.SourceGeneration/AssertInterpolatedStringAppendMethodsGenerator.cs Implements the incremental generator emitting append-method overloads per opt-in handler struct.

Copilot's findings

  • Files reviewed: 21/21 changed files
  • Comments generated: 4

Comment on lines +77 to +81
StructuredAssertionMessage structured = new(FrameworkMessages.ContainsItemFailedSummary);
structured.WithUserMessage(userMessage);
structured.WithEvidence(evidence);
structured.WithExpectedAndActual(expectedText, collectionText);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in cc157bfReportContainsFailed now calls WithExpectedAndActual(expectedText, null), leaving ActualText unset and keeping the collection only in the evidence block, matching Assert.Contains.

Comment on lines +145 to +149
StructuredAssertionMessage structured = new(FrameworkMessages.DoesNotContainItemFailedSummary);
structured.WithUserMessage(userMessage);
structured.WithEvidence(evidence);
structured.WithExpectedAndActual(notExpectedText, collectionText);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in cc157bfReportDoesNotContainFailed now calls WithExpectedAndActual(notExpectedText, null), matching Assert.DoesNotContain.

Comment thread test/UnitTests/TestFramework.UnitTests/Assertions/CollectionAssertTests.cs Outdated
Comment thread test/UnitTests/TestFramework.UnitTests/Assertions/CollectionAssertTests.cs Outdated

@Evangelink Evangelink left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note

🤖 Automated review 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 Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Expert Review — PR #9297: Use StructuredAssertionMessage for StringAssert and CollectionAssert membership

Verdict table

# Dimension Status Notes
1 Algorithmic Correctness Logic for AllItemsAreUnique / DoesNotContain is correct: [DoesNotReturn] helpers throw and unwind the foreach correctly.
2 Threading & Concurrency ✅ N/A No shared state introduced.
3 Security No path traversal, injection, or leaks.
4 Public API & Binary Compatibility No new public API surface; GenerateAssertInterpolatedStringAppendMethodsAttribute is internal; no init accessors introduced.
5 Performance Collection rendering is on the failure path only; AssertionValueRenderer already exists for this.
6 Cross-TFM Compatibility #if NETCOREAPP3_1_OR_GREATER guards the ReadOnlySpan<char> overloads in generated code; generator targets netstandard2.0.
7 Resource & IDisposable ✅ N/A
8 Defensive Coding
9 Localization & Resources All new summary strings are in FrameworkMessages.resx; no .xlf manually edited; no hard-coded user-facing strings.
10 Test Isolation No static mutable state.
11 Assertion Quality ⚠️ See inline comment — comparison: evidence line not tested.
12 Flakiness No time-dependent or port-dependent patterns.
13 Test Completeness ⚠️ See inline comment — comparison: line gap.
14 Data-Driven Tests ✅ N/A
15 Code Structure Extraction of Report*Failed helpers is clean; [DoesNotReturn] is correctly applied.
16 Naming & Conventions Naming consistent with existing ReportXxxFailed pattern.
17 Documentation Generator has a clear XML doc comment; attribute has an accurate doc comment.
18 Analyzer / Code Fix Quality ✅ N/A
19 IPC Wire Compatibility ✅ N/A
20 Build Infrastructure ⚠️ TestFramework.SourceGeneration.csproj missing from MSTest.slnf and NonWindowsTests.slnf — see inline comment.
21 Scope & PR Discipline ⚠️ Two distinct concerns (source generator + structured messages) in one PR, explicitly acknowledged in the description. See note below.
22 PowerShell Scripting ✅ N/A

Summary

The core migration from string.Format + FrameworkMessages.*Fail to StructuredAssertionMessage is clean, consistent, and follows the established pattern from the existing Assert.* methods. The structured evidence blocks for StringAssert and CollectionAssert.Membership are well-chosen and the ExpectedText / ActualText / Data["assert.expected"|"assert.actual"] population is verified by the new tests.

Three actionable findings (see inline comments):

  1. Generator null-safety (MODERATE)GetHandlerInfo does not guard against a top-level (non-nested) struct annotated with the attribute. structSymbol.ContainingType is nullable, and passing null to GetContainingTypeDeclaration causes an NullReferenceException at generator time that manifests as a confusing CS8785 diagnostic. The fix is a one-line predicate refinement or an early-return guard.

  2. Filtered solutions not updated (MODERATE)MSTest.slnf and NonWindowsTests.slnf are missing the new TestFramework.SourceGeneration.csproj. Every other generator project (MSTest.AotReflection.SourceGeneration, MSTest.SourceGeneration) appears in these solution filters. Leaving the new generator out makes it invisible to the IDE's solution explorer for anyone using the filtered solution view.

  3. Test gap on comparison: evidence line (MODERATE)ReportContainsFailed, ReportStartsWithFailed, and ReportEndsWithFailed each append comparison: <StringComparison> to the evidence block. This is new diagnostic output that no current test asserts on. A one-liner addition to three of the five new StringAssertXxxPopulatesStructuredExpectedAndActual tests would provide protection against future regressions.

Scope note: The source generator (removal of ~575 lines of boilerplate, new 183-line generator) is a self-contained, independently-useful change that the PR description itself labels "a separate concern." Merging it into the StringAssert/CollectionAssert migration makes the PR harder to review, bisect, and selectively revert. For future PRs, please consider splitting such combinations, but this alone is not a blocker for this PR given that the two changes are cohesive in authorship and effect.

No blocking issues found; the correctness, backwards-compatibility, and localization of the main change are solid.

Comment thread TestFx.slnx
@Evangelink

Copy link
Copy Markdown
Member Author

🧪 Test quality grade — PR #9297

28 tests graded (21 A, 7 B — nothing below B) across 3 files. The new AssertInterpolatedStringHandlerGeneratedOverloadsTests class and all new "Populates" tests are uniformly clean. The only recurring finding: 6 modified StringAssertTests methods catch the right exception but use the broad Exception type; their new "Populates" siblings already demonstrate the more precise AssertFailedException pattern.

ΔTestGradeBandNotes
new CollectionAssertTests.
CollectionAssertAllItemsAreNotNullPopulatesStructuredActual
B 80–89 Message content verified; ActualText and Data actual only checked for non-null — no value assertion on actual text.
mod StringAssertTests.
StringAssertContains
B 80–89 Exception + message check is functional; narrowing to AssertFailedException would add type precision.
mod StringAssertTests.
StringAssertContainsDoesNotThrowFormatException
B 80–89 Exception + message check is functional; narrowing to AssertFailedException would add type precision.
mod StringAssertTests.
StringAssertContainsDoesNotThrowFormatExceptionWithArguments
B 80–89 Exception + message check is functional; narrowing to AssertFailedException would add type precision.
mod StringAssertTests.
StringAssertDoesNotMatch
B 80–89 Exception + message check is functional; narrowing to AssertFailedException would add type precision.
mod StringAssertTests.
StringAssertEndsWith
B 80–89 Exception + message check is functional; narrowing to AssertFailedException would add type precision.
mod StringAssertTests.
StringAssertStartsWith
B 80–89 Exception + message check is functional; narrowing to AssertFailedException would add type precision.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
GenericHandler_
AreEqual_
RendersInterpolatedMessage
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
GenericValueWithAlignment_
IsRendered
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
GenericValueWithAlignmentAndFormat_
IsRendered
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
GenericValueWithFormat_
IsRendered
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
Handler_
IsNull_
RendersInterpolatedMessage
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
Literal_
AndGenericValue_
AreRendered
A 90–100 Clear AAA; exception assertion verifies AppendLiteral + AppendFormatted rendering end-to-end.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
NonGenericHandler_
IsFalse_
RendersInterpolatedMessage
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
ObjectValue_
IsRendered
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
ObjectValueWithAlignmentAndFormat_
IsRendered
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
ReadOnlySpanValue_
IsRendered
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
ReadOnlySpanValueWithAlignment_
IsRendered
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
StringValue_
IsRendered
A 90–100 No issues found.
new AssertInterpolatedStringHandlerGeneratedOverloadsTests.
StringValueWithAlignment_
IsRendered
A 90–100 No issues found.
new CollectionAssertTests.
CollectionAssertAllItemsAreUniquePopulatesStructuredActual
A 90–100 Two message-content checks cover the error text; ActualText and Data actual only checked for non-null.
new CollectionAssertTests.
CollectionAssertContainsPopulatesStructuredExpectedAndActual
A 90–100 Message, ExpectedText, and Data expected verified by equality; Data actual only checked for non-null.
new CollectionAssertTests.
CollectionAssertDoesNotContainPopulatesStructuredExpectedAndActual
A 90–100 Message, ExpectedText, and Data expected verified by equality; Data actual only checked for non-null.
new StringAssertTests.
StringAssertContainsPopulatesStructuredExpectedAndActual
A 90–100 Four equality assertions cover ExpectedText, ActualText, and both Data entries — complete structured-error coverage.
new StringAssertTests.
StringAssertDoesNotMatchPopulatesStructuredExpectedAndActual
A 90–100 No issues found.
new StringAssertTests.
StringAssertEndsWithPopulatesStructuredExpectedAndActual
A 90–100 No issues found.
new StringAssertTests.
StringAssertMatchesPopulatesStructuredExpectedAndActual
A 90–100 No issues found.
new StringAssertTests.
StringAssertStartsWithPopulatesStructuredExpectedAndActual
A 90–100 No 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. · 663.4 AIC · ⌖ 14.4 AIC · ⊞ 45.6K · [◷]( · )

Evangelink and others added 2 commits June 20, 2026 15:12
Drop ActualText for CollectionAssert membership failures and use LINQ Any in the source generator.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Align CollectionAssert.Contains/DoesNotContain membership failures with Assert.Contains/DoesNotContain by leaving ActualText unset (collection stays in the evidence block only)
- Update CollectionAssert tests to assert ActualText/assert.actual are absent
- Add comparison-line assertions to StringAssert Contains/StartsWith/EndsWith tests
- Simplify the source generator named-argument scan with LINQ Any
- Guard the generator predicate against top-level structs to avoid a NullReferenceException at generation time
- Add TestFramework.SourceGeneration to MSTest.slnf and NonWindowsTests.slnf

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 20, 2026 13:23

Copilot AI 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: 23/23 changed files
  • Comments generated: 1

Comment on lines +69 to +73
<ItemGroup>
<!-- Emits the repetitive AppendLiteral/AppendFormatted overloads for the Assert.*InterpolatedStringHandler
structs (see GenerateAssertInterpolatedStringAppendMethodsAttribute). -->
<ProjectReference Include="..\TestFramework.SourceGeneration\TestFramework.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

After merging latest main (7ac9689), this concern is largely moot: the source-generator project (TestFramework.SourceGeneration) and its Analyzer ProjectReference from TestFramework.csproj already landed on main via #9290, so they are no longer net-new in this PR — git diff origin/main -- TestFramework.csproj is now empty. The only remaining generator delta in this PR versus main is the two review-driven fixes (the nested-struct predicate guard and the LINQ Any refactor). So the PR's effective scope is back to the StringAssert/CollectionAssert membership StructuredAssertionMessage migration described in the title/description, with a minimal generator footprint.

…es-legacy-asserts-9275

# Conflicts:
#	src/TestFramework/TestFramework.SourceGeneration/AssertInterpolatedStringAppendMethodsGenerator.cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants