Skip to content

Server mode drops inner exceptions: error.message and error.stacktrace carry only the outermost exception #11202

Description

Summary

In server mode (the JSON-RPC protocol used by Visual Studio and Rider), a failed test node is serialised as error.message = Explanation ?? Exception.Message and error.stacktrace = Exception.StackTrace. The serialiser never walks Exception.InnerException, so inner exceptions are silently dropped from what the IDE shows. The console reporter and the dotnet test IPC consumer both flatten the chain, so the same failure looks complete on the CLI and incomplete in the IDE.

Proposal: flatten the inner-exception chain in the server-mode serialisers the same way ExceptionFlattener already does for the terminal, so a framework can hand the platform a raw exception and get consistent output on every client.

Background and Motivation

Both server-mode serialisers do the same thing for FailedTestNodeStateProperty, ErrorTestNodeStateProperty, TimeoutTestNodeStateProperty and CancelledTestNodeStateProperty:

  • src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/SerializerUtilities.TestNodeSerializers.cs
  • src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs
properties["error.message"] = failedTestNodeStateProperty.Explanation ?? failedTestNodeStateProperty.Exception?.Message;
Exception? exception = failedTestNodeStateProperty.Exception;
if (exception is not null)
{
    properties["error.stacktrace"] = exception.StackTrace ?? string.Empty;
}

By contrast:

  • OutputDevice/Terminal/ExceptionFlattener.Flatten(errorMessage, exception) walks InnerException (and AggregateException.Flatten().InnerExceptions) and the terminal reporter prints every level. This was the fix for InnerException's are lost in the Testing Platform output #3783 (PR Display inner exceptions #3920), which only touched the terminal output.
  • ServerMode/DotnetTest/IPC/DotnetTestDataConsumer.FlattenToExceptionMessages does the same for the dotnet test pipe, so dotnet test output is complete too.

The practical effect is that any test framework that passes the raw exception to the platform loses inner exceptions in IDEs only. This was reported against TUnit as thomhurst/TUnit#1327 and against Rider as RSRP-499947; JetBrains traced it to the framework side, which is fair given the current contract. Captured server-mode payload for a test throwing new Exception("Thrown from Method1", new ArgumentException("Thrown from Method2", new InvalidOperationException("Thrown from Method3"))):

error.message:    [Test Failure] Thrown from Method1
error.stacktrace:    at TUnit.TestProject.NestedExceptionTests.Method1() in ...\NestedExceptionTests.cs:line 22
                     at TUnit.TestProject.NestedExceptionTests.Test() in ...\NestedExceptionTests.cs:line 11
                     ...

Nothing from Method2 or Method3 reaches the IDE, while dotnet run and dotnet test print all three levels.

Every framework that works in IDEs today does so by pre-folding the chain into a wrapper exception before the platform sees it:

  • MSTest: MSTestTestNodeException(message, stackTrace) built from GetFormattedExceptionMessage() / GetStackTraceInformation().
  • xUnit v3: XunitException with ExceptionUtility.CombineMessages / CombineStackTraces.
  • NUnit (via Microsoft.Testing.Extensions.VSTestBridge): VSTestException(ErrorMessage, ErrorStackTrace) from NUnit's ExceptionHelper.BuildMessage / BuildStackTrace.
  • TUnit now does the same in fix: fold inner exceptions into IDE test failure output thomhurst/TUnit#6777.

That is four independent re-implementations of a concern the platform already owns for its other two output paths, and it is not documented anywhere that FailedTestNodeStateProperty.Exception must be pre-flattened for server mode.

Proposed Feature

In both server-mode serialisers, run the failure through ExceptionFlattener.Flatten(explanation, exception) and emit:

  • error.message: the first flat exception's message followed by one line per inner exception, for example ---> System.ArgumentException: Thrown from Method2, matching what the terminal reporter renders.
  • error.stacktrace: the outer stack trace followed by each inner stack trace under a separator such as --- Inner exception stack trace (System.ArgumentException) --- (or the .NET --- End of inner exception stack trace --- convention).

This is backwards compatible for frameworks that already pre-fold: their wrapper exceptions have InnerException == null, so flattening is a no-op and nothing is printed twice. No protocol change is needed and existing IDE clients need no update.

A longer-term option would be a structured error.exceptions array (type, message, stack trace, parent index, as DotnetTestDataConsumer already produces for the dotnet test pipe) gated on a client capability, so IDEs can render the chain as separate collapsible entries. The flattened strings above would remain the fallback.

Alternative Designs

  • Keep the current behaviour and document that Exception.Message and Exception.StackTrace must already contain the whole chain for server mode. That is the de facto contract today, but it is unwritten and every framework has had to discover it through user reports.
  • Provide a public FlattenedException helper in the platform for frameworks to wrap with, so the folding logic lives in one place even if the serialiser stays as it is.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions