Report MSTest assertion expected/actual through a platform property - #10353
Conversation
The terminal's Expected/Actual block and the assert.expected / assert.actual protocol fields were dead code for MSTest. Both production paths replace the original assertion exception with a synthetic one carrying only a message and stack trace string (MSTestTestNodeException, VSTestException), and the only channel for the structured values was Exception.Data, which those synthetic exceptions do not have. Every consumer therefore read null. Add an AssertionFailureProperty to Microsoft.Testing.Platform that carries the values as a first-class property instead of riding on Exception.Data. MSTest's TestResult captures them as strings when TestFailureException is assigned, so they survive an AppDomain boundary exactly like ExceptionMessage does, and the adapter publishes the property alongside the failed state. All five existing consumers prefer the property and fall back to Exception.Data for producers that have not been updated. The choice is all-or-nothing so the two halves of a diff always come from the same producer. The wire format is unchanged: same protocol keys, same ordering, same pipe field ids. Values are only reported when the result contains exactly one assertion failure carrying a comparison. Assert.Scope throws a single AssertFailedException wrapping an AggregateException of every collected failure, and assert.expected / assert.actual carry no label, so two competing comparisons cannot both be reported and the survivor would look authoritative. Failures with no comparison (Assert.Fail, or any non-assertion exception) do not count, so a cleanup Assert.Fail does not hide the body's diff. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01779869-b141-4244-aac9-d4f2c1da3a1a
There was a problem hiding this comment.
Pull request overview
Adds structured MSTest assertion values to MTP so reporters can display expected/actual diffs without relying on Exception.Data.
Changes:
- Introduces and publishes
AssertionFailureProperty. - Updates terminal, MSBuild, pipe, and JSON consumers.
- Adds unit and acceptance coverage for assertion reporting.
Show a summary per file
| File | Description |
|---|---|
test/UnitTests/TestFramework.UnitTests/TestResultTests.cs |
Tests assertion extraction and ambiguity rules. |
test/UnitTests/MSTestAdapter.UnitTests/MSTestTestNodeConverterTests.cs |
Tests property publication by MSTest. |
test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/FormatterUtilitiesTests.cs |
Tests JSON assertion fields. |
test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/DotnetTestDataConsumerTests.cs |
Tests pipe consumer extraction. |
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/AssertionFailurePropertyTests.cs |
Tests the new property API. |
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs |
Verifies scoped assertion output. |
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs |
Verifies terminal diff rendering. |
src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt |
Tracks new internal result members. |
src/TestFramework/TestFramework/Attributes/TestMethod/TestResult.cs |
Captures unambiguous assertion values. |
src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/SerializerUtilities.TestNodeSerializers.cs |
Serializes property values through Jsonite. |
src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs |
Serializes property values through System.Text.Json. |
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/DotnetTestDataConsumer.cs |
Forwards values through the pipe protocol. |
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt |
Tracks the new public property. |
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.DataConsumption.cs |
Supplies values to terminal reporting. |
src/Platform/Microsoft.Testing.Platform/Messages/AssertionFailureProperty.cs |
Defines the structured assertion property. |
src/Platform/Microsoft.Testing.Extensions.MSBuild/MSBuildConsumer.cs |
Supplies values to MSBuild reporting. |
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestNodeConverter.cs |
Publishes captured MSTest values. |
Review details
Suppressed comments (1)
src/TestFramework/TestFramework/Attributes/TestMethod/TestResult.cs:265
- The depth cutoff can make a multi-comparison result look unambiguous. For example, if one aggregate branch contains a shallow comparison and another comparison remains beyond
MaxDepth, the latter is ignored and this method returns1, so the shallow pair is published as authoritative. Reaching the cutoff with an unvisited exception should conservatively mark the result ambiguous (or otherwise suppress the pair).
exception = exception.InnerException;
depth++;
- Files reviewed: 17/17 changed files
- Comments generated: 2
- Review effort level: Balanced
…assertion properties Math.Min keeps the stored count matching its documented 'capped at 2' invariant now that the field is serialized. In the terminal walk a duplicate AssertionFailureProperty now suppresses the diff instead of last-wins. That walk deliberately replaced SingleOrDefault with a non-throwing single pass, so it drops the value rather than failing, which also matches how MSTest suppresses competing comparisons at the source. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01779869-b141-4244-aac9-d4f2c1da3a1a
ca7f14c to
c7a7d82
Compare
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.DataConsumption.cs:260
- When duplicate assertion properties are encountered, the loop deliberately sets
assertionFailureto null while leavinghasAssertionFailuretrue. These checks then fall back toException.Data, so a malformed node can still render a legacy diff instead of suppressing the ambiguous values. Gate the fallback on whether any assertion property was seen.
expected: assertionFailure is not null ? assertionFailure.Expected : failedState.Exception?.Data["assert.expected"] as string,
actual: assertionFailure is not null ? assertionFailure.Actual : failedState.Exception?.Data["assert.actual"] as string,
- Files reviewed: 17/17 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 150.2 AIC · ⌖ 4.04 AIC · ⊞ 11.8K · ◷
Serialize_FailedTestNodeWithAssertionFailureProperty_PrefersPropertyOverExceptionData only checked that the property values were present, so an implementation emitting both channels would have passed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01779869-b141-4244-aac9-d4f2c1da3a1a
…Data A duplicate AssertionFailureProperty resolves the local to null to suppress the diff, but the consumption site tested that null and so fell through to the legacy Exception.Data values - rendering exactly the arbitrary diff the suppression exists to prevent. Gate on whether a property was seen instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01779869-b141-4244-aac9-d4f2c1da3a1a
|
Picking up the suppressed comment from the latest review, since it found a real bug and has no thread to reply on:
Correct, and it defeated the suppression I added in c7a7d82: a duplicate resolved the local to null, and the consumption site tested that null, so it fell straight through to the legacy Fixed in 9b6c7ec by gating on whether a property was seen rather than on the resolved value: expected: hasAssertionFailure ? assertionFailure?.Expected : failedState.Exception?.Data["assert.expected"] as string,Only the terminal path needed this. The other four consumers reject duplicates outright via |
…zation TestResult is [Serializable] on .NET Framework, so newly added fields are required during deserialization by default and a payload written by a build that predates them would throw SerializationException. OptionalFieldAttribute lets those payloads default to zero/null instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01779869-b141-4244-aac9-d4f2c1da3a1a
🧪 Test quality grade — PR #10353
This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with
|
Replace != null comparisons with 'is not null' pattern matching in MSBuildConsumer.cs and SerializerUtilities.TestNodeSerializers.cs, both newly added in PR #10353. This aligns with the project coding standards enforced across the Platform codebase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…10362) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ed3dfd9e-f272-4b5a-b0fe-243680d50305
Summary
The terminal's
Expected:/Actual:block and theassert.expected/assert.actualprotocol fields have been dead code for MSTest. Both paths that build a failedTestNodereplace the original assertion exception with a synthetic one carrying only a message and stack trace string (MSTestTestNodeException,VSTestException), and the only channel for the structured values wasException.Data, which those synthetic exceptions do not have. So all five consumers readnulland MSTest users never saw the diff block, even though the renderer was fully implemented.Verified with an A/B run of
samples/PlaygroundonAssert.AreEqual(5, 2):Approach
Add an
AssertionFailurePropertyto Microsoft.Testing.Platform so the values are a first-class property instead of riding onException.Data:TestResultrecords the values as strings whenTestFailureExceptionis assigned, for the same reasonExceptionMessage/ExceptionStackTraceare recorded: the exception instance does not survive an AppDomain boundary and the adapter reports from these strings. The chain is walked because the adapter always wraps the assertion inTestFailedException, so the outermost exception is never the one holding the values.MSTestTestNodeConverterpublishes the property alongside the failed state.dotnet testpipe and both server-mode serializers prefer the property and fall back toException.Datafor producers that have not been updated. The choice is all-or-nothing per node so the two halves of a diff always come from the same producer.The wire format is deliberately unchanged: same protocol keys, same key ordering, same pipe field ids, so no protocol doc or golden-string churn. What the property adds is the ability to emit those keys when there is no exception at all, which
Exception.Datastructurally cannot express (relevant forFailedTestNodeStateProperty(string explanation), added in #2536 precisely so frameworks can fail without an exception).Reporting policy worth a careful look
Values are reported only when the result contains exactly one assertion failure carrying a comparison:
Assert.Scope()throws a singleAssertFailedExceptionwrapping anAggregateExceptionof every collected failure. Sinceassert.expected/assert.actualcarry no label, two competing comparisons cannot both be reported and whichever survived would look authoritative next to a message saying "N assertion(s) failed". The count is accumulated across the whole result becauseTestFailureExceptioncan be assigned more than once (TestInitialize plus TestCleanup).Failures that carry no comparison (
Assert.Fail, or any non-assertion exception) deliberately do not count, so a cleanupAssert.Faildoes not hide the body's diff while a cleanupthrowwould not. NoteAssert.IsTrue/IsFalsedo carry a comparison (true/false), so a scope mixingAreEqualandIsTrueis suppressed.A list-shaped property that could carry N diffs properly is the better long-term answer, but it is a much larger public API surface and is out of scope here.
Notes for reviewers
O(1)SingleOrDefault<TestNodeStateProperty>()state check so discovery, in-progress and passed nodes never pay for a linked-list walk.SerializerUtilities.TestNodeSerializers.cspicked up a UTF-8 BOM on line 1 (the file had none). That matches the repo's stated encoding policy for rewritten.csfiles, but happy to drop it if the unrelated diff line is unwanted.Validation
Full build clean. Unit tests:
Microsoft.Testing.Platform.UnitTests1940 on net9.0 and 1899 on net462 (covering both the System.Text.Json and Jsonite serializers),TestFramework.UnitTests1502,MSTestAdapter.PlatformServices.UnitTests1062,Microsoft.Testing.Extensions.UnitTests848,MSTestAdapter.UnitTests64,Microsoft.Testing.Platform.MSBuild.UnitTests20.Acceptance tests after
-pack:OutputTestsandSoftAssertionTests13/13 across net462, net8.0 and net10.0, including new guards that the block is rendered for a single-failure scope and is not rendered for a multi-failure one.