From 673e50f65b3b75d55c02ff588ca193dcf2a81e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 11 Jun 2026 13:30:20 +0200 Subject: [PATCH 1/3] Split Json.cs into partial-class files Refactors the 789-line ServerMode/JsonRpc/Json/Json.cs into four partial files, each well under the 350-line acceptance criterion from #9017: - Json.cs (210 lines): core engine (constructor, SerializeAsync, Deserialize, Bind/TryBind/TryArrayBind, ValidateJsonRpcHeader) - Json.Serializers.cs (173 lines): RegisterDefaultSerializers - Json.Deserializers.cs (247 lines): RegisterDefaultDeserializers - Json.TestNodeSerializer.cs (198 lines): BuildTestNodeProperties helper (split out the large TestNode property-building lambda) The class is now declared internal sealed partial class Json. The new files are also added to the netstandard2.0 exclusion list in Microsoft.Testing.Platform.csproj. No behavioral changes. All 40 JsonTests/JsoniteTests/FormatterUtilities tests pass. Build is clean across net8.0, net9.0, and netstandard2.0. Fixes #9017 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Testing.Platform.csproj | 3 + .../JsonRpc/Json/Json.Deserializers.cs | 247 ++++++++ .../JsonRpc/Json/Json.Serializers.cs | 173 ++++++ .../JsonRpc/Json/Json.TestNodeSerializer.cs | 198 ++++++ .../ServerMode/JsonRpc/Json/Json.cs | 585 +----------------- 5 files changed, 624 insertions(+), 582 deletions(-) create mode 100644 src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs create mode 100644 src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs create mode 100644 src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs diff --git a/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj b/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj index 9c400d8455..7efc519e10 100644 --- a/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj +++ b/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj @@ -24,6 +24,9 @@ This package provides the core platform and the .NET implementation of the proto + + + diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs new file mode 100644 index 0000000000..537b418899 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.Json; + +using Microsoft.Testing.Platform.Extensions.Messages; + +namespace Microsoft.Testing.Platform.ServerMode.Json; + +internal sealed partial class Json +{ + private static void RegisterDefaultDeserializers(Dictionary deserializers) + { + // Deserializers + deserializers[typeof(string)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetString()!); + deserializers[typeof(bool)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetBoolean()); + deserializers[typeof(int)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetInt32()); + deserializers[typeof(decimal)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetDecimal()); + deserializers[typeof(DateTime)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetDateTime()); + + deserializers[typeof(IDictionary)] = new JsonElementDeserializer>((json, jsonDocument) => + { + Dictionary items = []; + foreach (JsonProperty kvp in jsonDocument.EnumerateObject()) + { + switch (kvp.Value.ValueKind) + { + case JsonValueKind.String: + items.Add(kvp.Name, kvp.Value.GetString()); + break; + case JsonValueKind.Number: + items.Add(kvp.Name, kvp.Value.GetInt32()); + break; + case JsonValueKind.True: + items.Add(kvp.Name, true); + break; + case JsonValueKind.False: + items.Add(kvp.Name, false); + break; + case JsonValueKind.Object: + items.Add(kvp.Name, json.Bind>(kvp.Value)); + break; + case JsonValueKind.Array: + items.Add(kvp.Name, json.Bind(kvp.Value)); + break; + case JsonValueKind.Null: + items.Add(kvp.Name, null); + break; + default: + throw new InvalidOperationException($"key: {kvp.Name}, value: {kvp.Value}, type: {kvp.Value.ValueKind}"); + } + } + + return items; + }); + + deserializers[typeof(RpcMessage)] = new JsonElementDeserializer((json, jsonElement) => + { + ValidateJsonRpcHeader(json, jsonElement); + + if (json.TryBind(jsonElement, out string? method, JsonRpcStrings.Method)) + { + bool hasId = json.TryBind(jsonElement, out int id, JsonRpcStrings.Id); + + object? @params = null; + if (jsonElement.TryGetProperty(JsonRpcStrings.Params, out JsonElement value)) + { + try + { + // Parse the specific methods + @params = method switch + { + JsonRpcMethods.Initialize => json.Bind(value), + JsonRpcMethods.TestingDiscoverTests => json.Bind(value), + JsonRpcMethods.TestingRunTests => json.Bind(value), + JsonRpcMethods.CancelRequest => json.Bind(value), + JsonRpcMethods.Exit => json.Bind(value), + + // Note: Let the server report unknown RPC request back to the client. + _ => null, + }; + } + catch (Exception ex) when (ex is MessageFormatException or InvalidOperationException or JsonException) + { + // If params can't be deserialized for a request, capture the failure so + // we can later send back a properly coded JSON-RPC error using the request id. + // For notifications there's no one to respond to, but we still avoid + // crashing the message-handling loop by swallowing into the sentinel. + // We catch the broader set of deserialization-related exceptions because the + // request payload is untrusted client input and the lower-level helpers + // (e.g. JsonElement.GetString() on a non-string element) can throw types + // other than MessageFormatException. + @params = new InvalidRequestParamsArgs(ErrorCodes.InvalidParams, ex.Message); + } + } + + return hasId + ? new RequestMessage(id, method!, @params) + : new NotificationMessage(method!, @params); + } + + if (jsonElement.TryGetProperty(JsonRpcStrings.Result, out JsonElement element)) + { + // Note: Because the result message does not contain the original method name, + // it's not possible for us to do a typed deserialization. + // The best option we've got is to return a generic property bag. + int id = json.Bind(jsonElement, JsonRpcStrings.Id); + + IDictionary? result = element.ValueKind == JsonValueKind.Null ? null : + json.Bind>(jsonElement, JsonRpcStrings.Result); + + return new ResponseMessage(id, result); + } + + return json.TryBind(jsonElement, out ErrorMessage? errorMessage) ? errorMessage! : throw new MessageFormatException(); + }); + + deserializers[typeof(InitializeRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => new InitializeRequestArgs( + ProcessId: json.Bind(jsonElement, JsonRpcStrings.ProcessId), + ClientInfo: json.Bind(jsonElement, JsonRpcStrings.ClientInfo), + Capabilities: json.Bind(jsonElement, JsonRpcStrings.Capabilities))); + + deserializers[typeof(ClientInfo)] = new JsonElementDeserializer((json, jsonElement) => new ClientInfo( + Name: json.Bind(jsonElement, JsonRpcStrings.Name), + Version: json.Bind(jsonElement, JsonRpcStrings.Version))); + + deserializers[typeof(ClientCapabilities)] = new JsonElementDeserializer((json, jsonElement) => + { + jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing); + + return new ClientCapabilities( + DebuggerProvider: json.Bind(testing, JsonRpcStrings.DebuggerProvider)); + }); + + deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer( + (json, jsonElement) => new InitializeResponseArgs( + ProcessId: json.Bind(jsonElement, JsonRpcStrings.ProcessId), + ServerInfo: json.Bind(jsonElement, JsonRpcStrings.ServerInfo), + Capabilities: json.Bind(jsonElement, JsonRpcStrings.Capabilities))); + + deserializers[typeof(ServerInfo)] = new JsonElementDeserializer( + (json, jsonElement) => new ServerInfo( + Name: json.Bind(jsonElement, JsonRpcStrings.Name), + Version: json.Bind(jsonElement, JsonRpcStrings.Version))); + + deserializers[typeof(ServerCapabilities)] = new JsonElementDeserializer( + (json, jsonElement) => new ServerCapabilities( + TestingCapabilities: json.Bind(jsonElement, JsonRpcStrings.Testing))); + + deserializers[typeof(ServerTestingCapabilities)] = new JsonElementDeserializer( + (json, jsonElement) => new ServerTestingCapabilities( + SupportsDiscovery: json.Bind(jsonElement, JsonRpcStrings.SupportsDiscovery), + MultiRequestSupport: json.Bind(jsonElement, JsonRpcStrings.MultiRequestSupport), + VSTestProviderSupport: json.Bind(jsonElement, JsonRpcStrings.VSTestProviderSupport), + SupportsAttachments: json.Bind(jsonElement, JsonRpcStrings.AttachmentsSupport), + MultiConnectionProvider: json.Bind(jsonElement, JsonRpcStrings.MultiConnectionProvider))); + + deserializers[typeof(DiscoverRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => + { + string runId = json.Bind(jsonElement, JsonRpcStrings.RunId); + if (!Guid.TryParse(runId, out Guid result)) + { + throw new MessageFormatException(JsonRpcStrings.InvalidRunIdErrorMessage); + } + + json.TryArrayBind(jsonElement, out TestNode[]? testNodes, JsonRpcStrings.Tests); + json.TryBind(jsonElement, out string? graphFilter, JsonRpcStrings.Filter); + + return new DiscoverRequestArgs( + RunId: result, + TestNodes: testNodes, + GraphFilter: graphFilter); + }); + + deserializers[typeof(RunRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => + { + string runId = json.Bind(jsonElement, JsonRpcStrings.RunId); + if (!Guid.TryParse(runId, out Guid result)) + { + throw new MessageFormatException(JsonRpcStrings.InvalidRunIdErrorMessage); + } + + json.TryArrayBind(jsonElement, out TestNode[]? testNodes, JsonRpcStrings.Tests); + json.TryBind(jsonElement, out string? graphFilter, JsonRpcStrings.Filter); + + return new RunRequestArgs( + RunId: result, + TestNodes: testNodes, + GraphFilter: graphFilter); + }); + + deserializers[typeof(TestNode)] = new JsonElementDeserializer( + (json, properties) => + { + PropertyBag propertyBag = new(); + string uid = json.Bind(properties, JsonRpcStrings.Uid) ?? string.Empty; + string displayName = json.Bind(properties, JsonRpcStrings.DisplayName); + + if (json.TryBind(properties, out string? locationFile, "location.file")) + { + json.TryBind(properties, out int locationLineStart, "location.line-start"); + json.TryBind(properties, out int locationLineEnd, "location.line-end"); + + TestFileLocationProperty testFileLocationProperty = new( + locationFile!, + new LinePositionSpan(new LinePosition(locationLineStart, 0), new LinePosition(locationLineEnd, 0))); + propertyBag.Add(testFileLocationProperty); + } + + return new TestNode + { + Uid = new TestNodeUid(uid), + DisplayName = displayName, + Properties = propertyBag, + }; + }); + + deserializers[typeof(CancelRequestArgs)] = new JsonElementDeserializer( + (json, jsonElement) => json.TryBind(jsonElement, out int id, JsonRpcStrings.Id) ? new CancelRequestArgs(id) : throw new MessageFormatException("id field should be an int")); + + deserializers[typeof(ExitRequestArgs)] = new JsonElementDeserializer( + (json, jsonElement) => new ExitRequestArgs()); + + deserializers[typeof(ErrorMessage)] = new JsonElementDeserializer( + (json, jsonElement) => + { + ValidateJsonRpcHeader(json, jsonElement); + + int id = json.Bind(jsonElement, JsonRpcStrings.Id); + JsonElement error = jsonElement.GetProperty(JsonRpcStrings.Error); + + int code = json.Bind(error, JsonRpcStrings.Code); + string message = json.Bind(error, JsonRpcStrings.Message); + + if (json.TryBind(error, out IDictionary? data, JsonRpcStrings.Data) && data?.Count == 0) + { + data = null; + } + + return new ErrorMessage( + Id: id, + ErrorCode: code, + Message: message ?? string.Empty, + Data: data); + }); + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs new file mode 100644 index 0000000000..d4c7fa1598 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Extensions.Messages; + +namespace Microsoft.Testing.Platform.ServerMode.Json; + +internal sealed partial class Json +{ + private static void RegisterDefaultSerializers(Dictionary serializers) + { + // Overridden default serializers for better performance using .NET runtime serialization APIs + + // Serialize response types. + serializers[typeof(RequestMessage)] = new JsonObjectSerializer(request => + [ + (JsonRpcStrings.JsonRpc, "2.0"), + (JsonRpcStrings.Id, request.Id), + (JsonRpcStrings.Method, request.Method), + (JsonRpcStrings.Params, request.Params) + ]); + + serializers[typeof(ResponseMessage)] = new JsonObjectSerializer(response => + [ + (JsonRpcStrings.JsonRpc, "2.0"), + (JsonRpcStrings.Id, response.Id), + (JsonRpcStrings.Result, response.Result) + ]); + + serializers[typeof(NotificationMessage)] = new JsonObjectSerializer(notification => + [ + (JsonRpcStrings.JsonRpc, "2.0"), + (JsonRpcStrings.Method, notification.Method), + (JsonRpcStrings.Params, notification.Params) + ]); + + serializers[typeof(ErrorMessage)] = new JsonObjectSerializer(error => + { + var errorMsg = new (string, object?)[] + { + (JsonRpcStrings.Code, error.ErrorCode), + (JsonRpcStrings.Data, error.Data ?? new()), + (JsonRpcStrings.Message, error.Message), + }; + + return + [ + (JsonRpcStrings.JsonRpc, "2.0"), + (JsonRpcStrings.Code, error.ErrorCode), + (JsonRpcStrings.Id, error.Id), + (JsonRpcStrings.Error, errorMsg) + ]; + }); + + serializers[typeof(InitializeResponseArgs)] = new JsonObjectSerializer(response => + [ + (JsonRpcStrings.ProcessId, response.ProcessId), + (JsonRpcStrings.ServerInfo, response.ServerInfo), + (JsonRpcStrings.Capabilities, response.Capabilities) + ]); + + serializers[typeof(ServerInfo)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.Name, info.Name), + (JsonRpcStrings.Version, info.Version) + ]); + + serializers[typeof(ServerCapabilities)] = new JsonObjectSerializer(capabilities => + [ + (JsonRpcStrings.Testing, capabilities.TestingCapabilities) + ]); + + serializers[typeof(ServerTestingCapabilities)] = new JsonObjectSerializer(capabilities => + [ + (JsonRpcStrings.SupportsDiscovery, capabilities.SupportsDiscovery), + (JsonRpcStrings.MultiRequestSupport, capabilities.MultiRequestSupport), + (JsonRpcStrings.VSTestProviderSupport, capabilities.VSTestProviderSupport), + (JsonRpcStrings.AttachmentsSupport, capabilities.SupportsAttachments), + (JsonRpcStrings.MultiConnectionProvider, capabilities.MultiConnectionProvider) + ]); + + serializers[typeof(Artifact)] = new JsonObjectSerializer(artifact => + [ + (JsonRpcStrings.Uri, artifact.Uri), + (JsonRpcStrings.Producer, artifact.Producer), + (JsonRpcStrings.Type, artifact.Type), + (JsonRpcStrings.DisplayName, artifact.DisplayName), + (JsonRpcStrings.Description, artifact.Description) + ]); + + serializers[typeof(DiscoverResponseArgs)] = new JsonObjectSerializer(response => []); + + serializers[typeof(RunResponseArgs)] = new JsonObjectSerializer(response => + [ + (JsonRpcStrings.Attachments, response.Artifacts) + ]); + + serializers[typeof(TestNodeUpdateMessage)] = new JsonObjectSerializer(message => + [ + (JsonRpcStrings.Node, message.TestNode), + (JsonRpcStrings.Parent, message.ParentTestNodeUid?.Value) + ]); + + serializers[typeof(TestNodeStateChangedEventArgs)] = new JsonObjectSerializer(message => + [ + (JsonRpcStrings.RunId, message.RunId), + (JsonRpcStrings.Changes, message.Changes) + ]); + + serializers[typeof(TestNode)] = new JsonObjectSerializer(BuildTestNodeProperties); + + serializers[typeof(LogEventArgs)] = new JsonObjectSerializer(message => + [ + (JsonRpcStrings.Level, message.LogMessage.Level.ToString()), + (JsonRpcStrings.Message, message.LogMessage.Message) + ]); + + serializers[typeof(CancelRequestArgs)] = new JsonObjectSerializer(request => + [ + (JsonRpcStrings.Id, request.CancelRequestId) + ]); + + serializers[typeof(TelemetryEventArgs)] = new JsonObjectSerializer(ev => + [ + (JsonRpcStrings.EventName, ev.EventName), + (JsonRpcStrings.Metrics, ev.Metrics) + ]); + + serializers[typeof(ProcessInfoArgs)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.Program, info.Program), + (JsonRpcStrings.Args, info.Args), + (JsonRpcStrings.WorkingDirectory, info.WorkingDirectory), + (JsonRpcStrings.EnvironmentVariables, info.EnvironmentVariables) + ]); + + serializers[typeof(AttachDebuggerInfoArgs)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.ProcessId, info.ProcessId) + ]); + + serializers[typeof(TestsAttachments)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.Attachments, info.Attachments) + ]); + + serializers[typeof(RunTestAttachment)] = new JsonObjectSerializer(info => + [ + (JsonRpcStrings.Uri, info.Uri), + (JsonRpcStrings.Producer, info.Producer), + (JsonRpcStrings.Type, info.Type), + (JsonRpcStrings.DisplayName, info.DisplayName), + (JsonRpcStrings.Description, info.Description) + ]); + + // Serializers + serializers[typeof(string)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v)); + serializers[typeof(bool)] = new JsonValueSerializer((w, v) => w.WriteBooleanValue(v)); + serializers[typeof(char)] = new JsonValueSerializer((w, v) => w.WriteStringValue($"{v}")); + serializers[typeof(int)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(long)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(float)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(double)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(decimal)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); + serializers[typeof(Guid)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v)); + serializers[typeof(DateTime)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); + serializers[typeof(DateTimeOffset)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); + + // _serializers[typeof(TimeSpan)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v.ToString())); // Remove for now + serializers[typeof((string, object?)[])] = new JsonObjectSerializer<(string, object?)[]>(n => n); + serializers[typeof(Dictionary)] = new JsonObjectSerializer>(d => [.. d.Select(kvp => (kvp.Key, (object?)kvp.Value))]); + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs new file mode 100644 index 0000000000..30668285a0 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.TestNodeSerializer.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Extensions.Messages; + +namespace Microsoft.Testing.Platform.ServerMode.Json; + +internal sealed partial class Json +{ + private static (string Name, object? Value)[] BuildTestNodeProperties(TestNode message) + { + List<(string Name, object? Value)> properties = + [ + with(capacity: 16), + (JsonRpcStrings.Uid, message.Uid.Value), + (JsonRpcStrings.DisplayName, message.DisplayName) + ]; + + List>? traits = null; + bool hasActionNodeType = false; + + int attachmentIndex = 0; + foreach (IProperty property in message.Properties) + { + if (property is TestMetadataProperty metadataProperty) + { + (traits ??= []).Add(new KeyValuePair(metadataProperty.Key, metadataProperty.Value)); + continue; + } + + if (property is SerializableKeyValuePairStringProperty keyValuePairProperty) + { + properties.Add((keyValuePairProperty.Key, keyValuePairProperty.Value)); + continue; + } + + if (property is TestFileLocationProperty fileLocationProperty) + { + properties.Add(("location.file", fileLocationProperty.FilePath)); + properties.Add(("location.line-start", fileLocationProperty.LineSpan.Start.Line)); + properties.Add(("location.line-end", fileLocationProperty.LineSpan.End.Line)); + continue; + } + + if (property is TestMethodIdentifierProperty testMethodIdentifierProperty) + { + properties.Add(("location.type", RoslynString.IsNullOrEmpty(testMethodIdentifierProperty.Namespace) + ? testMethodIdentifierProperty.TypeName + : $"{testMethodIdentifierProperty.Namespace}.{testMethodIdentifierProperty.TypeName}")); + + properties.Add(("location.method", testMethodIdentifierProperty.ParameterTypeFullNames.Length > 0 + ? $"{testMethodIdentifierProperty.MethodName}({string.Join(',', testMethodIdentifierProperty.ParameterTypeFullNames)})" + : testMethodIdentifierProperty.MethodName)); + + properties.Add(("location.method-arity", testMethodIdentifierProperty.MethodArity)); + + continue; + } + + if (property is StandardOutputProperty standardOutputProperty) + { + properties.Add(("standardOutput", standardOutputProperty.StandardOutput)); + } + + if (property is StandardErrorProperty standardErrorProperty) + { + properties.Add(("standardError", standardErrorProperty.StandardError)); + } + + if (property is TestNodeStateProperty testNodeStateProperty) + { + properties.Add(("node-type", "action")); + hasActionNodeType = true; + switch (property) + { + case DiscoveredTestNodeStateProperty: + { + properties.Add(("execution-state", "discovered")); + break; + } + + case InProgressTestNodeStateProperty: + { + properties.Add(("execution-state", "in-progress")); + break; + } + + case PassedTestNodeStateProperty: + { + properties.Add(("execution-state", "passed")); + break; + } + + case SkippedTestNodeStateProperty skippedTestNodeStateProperty: + { + properties.Add(("execution-state", "skipped")); + if (!RoslynString.IsNullOrEmpty(skippedTestNodeStateProperty.Explanation)) + { + properties.Add(("error.message", skippedTestNodeStateProperty.Explanation)); + } + + break; + } + + case FailedTestNodeStateProperty failedTestNodeStateProperty: + { + properties.Add(("execution-state", "failed")); + Exception? exception = failedTestNodeStateProperty.Exception; + properties.Add(("error.message", failedTestNodeStateProperty.Explanation ?? exception?.Message)); + if (exception is not null) + { + properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); + properties.Add(("assert.actual", exception.Data["assert.actual"] ?? string.Empty)); + properties.Add(("assert.expected", exception.Data["assert.expected"] ?? string.Empty)); + } + + break; + } + + case TimeoutTestNodeStateProperty timeoutTestNodeStateProperty: + { + properties.Add(("execution-state", "timed-out")); + Exception? exception = timeoutTestNodeStateProperty.Exception; + properties.Add(("error.message", timeoutTestNodeStateProperty.Explanation ?? exception?.Message)); + if (exception is not null) + { + properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); + } + + break; + } + + case ErrorTestNodeStateProperty errorTestNodeStateProperty: + { + properties.Add(("execution-state", "error")); + Exception? exception = errorTestNodeStateProperty.Exception; + properties.Add(("error.message", errorTestNodeStateProperty.Explanation ?? exception?.Message)); + if (exception is not null) + { + properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); + } + + break; + } + +#pragma warning disable CS0618, MTP0001 // Type or member is obsolete + case CancelledTestNodeStateProperty canceledTestNodeStateProperty: +#pragma warning restore CS0618, MTP0001 // Type or member is obsolete + { + properties.Add(("execution-state", "canceled")); + Exception? exception = canceledTestNodeStateProperty.Exception; + properties.Add(("error.message", canceledTestNodeStateProperty.Explanation ?? exception?.Message)); + if (exception is not null) + { + properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); + } + + break; + } + + default: + throw new NotSupportedException($"Unsupported TestNodeStateProperty '{testNodeStateProperty.GetType()}'"); + } + + continue; + } + + if (property is TimingProperty timingProperty) + { + properties.Add(("time.duration-ms", timingProperty.GlobalTiming.Duration.TotalMilliseconds)); + continue; + } + + if (property is FileArtifactProperty artifact) + { + properties.Add(($"attachments.{attachmentIndex}.uri", artifact.FileInfo.FullName)); + properties.Add(($"attachments.{attachmentIndex}.display-name", artifact.DisplayName)); + properties.Add(($"attachments.{attachmentIndex}.description", artifact.Description)); + attachmentIndex++; + continue; + } + } + + if (traits is not null) + { + // Insert "traits" right after "uid" and "display-name" to preserve the + // original wire format ordering. + properties.Insert(2, ("traits", traits)); + } + + if (!hasActionNodeType) + { + properties.Add(("node-type", "group")); + } + + return [.. properties]; + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs index b1ef45bc4d..53db3220ae 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs @@ -3,12 +3,11 @@ using System.Text.Json; -using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Helpers; namespace Microsoft.Testing.Platform.ServerMode.Json; -internal sealed class Json +internal sealed partial class Json { private readonly Dictionary _deserializers = []; private readonly Dictionary _serializers = []; @@ -16,586 +15,8 @@ internal sealed class Json public Json(Dictionary? serializers = null, Dictionary? deserializers = null) { - // Overridden default serializers for better performance using .NET runtime serialization APIs - - // Serialize response types. - _serializers[typeof(RequestMessage)] = new JsonObjectSerializer(request => - [ - (JsonRpcStrings.JsonRpc, "2.0"), - (JsonRpcStrings.Id, request.Id), - (JsonRpcStrings.Method, request.Method), - (JsonRpcStrings.Params, request.Params) - ]); - - _serializers[typeof(ResponseMessage)] = new JsonObjectSerializer(response => - [ - (JsonRpcStrings.JsonRpc, "2.0"), - (JsonRpcStrings.Id, response.Id), - (JsonRpcStrings.Result, response.Result) - ]); - - _serializers[typeof(NotificationMessage)] = new JsonObjectSerializer(notification => - [ - (JsonRpcStrings.JsonRpc, "2.0"), - (JsonRpcStrings.Method, notification.Method), - (JsonRpcStrings.Params, notification.Params) - ]); - - _serializers[typeof(ErrorMessage)] = new JsonObjectSerializer(error => - { - var errorMsg = new (string, object?)[] - { - (JsonRpcStrings.Code, error.ErrorCode), - (JsonRpcStrings.Data, error.Data ?? new()), - (JsonRpcStrings.Message, error.Message), - }; - - return - [ - (JsonRpcStrings.JsonRpc, "2.0"), - (JsonRpcStrings.Code, error.ErrorCode), - (JsonRpcStrings.Id, error.Id), - (JsonRpcStrings.Error, errorMsg) - ]; - }); - - _serializers[typeof(InitializeResponseArgs)] = new JsonObjectSerializer(response => - [ - (JsonRpcStrings.ProcessId, response.ProcessId), - (JsonRpcStrings.ServerInfo, response.ServerInfo), - (JsonRpcStrings.Capabilities, response.Capabilities) - ]); - - _serializers[typeof(ServerInfo)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.Name, info.Name), - (JsonRpcStrings.Version, info.Version) - ]); - - _serializers[typeof(ServerCapabilities)] = new JsonObjectSerializer(capabilities => - [ - (JsonRpcStrings.Testing, capabilities.TestingCapabilities) - ]); - - _serializers[typeof(ServerTestingCapabilities)] = new JsonObjectSerializer(capabilities => - [ - (JsonRpcStrings.SupportsDiscovery, capabilities.SupportsDiscovery), - (JsonRpcStrings.MultiRequestSupport, capabilities.MultiRequestSupport), - (JsonRpcStrings.VSTestProviderSupport, capabilities.VSTestProviderSupport), - (JsonRpcStrings.AttachmentsSupport, capabilities.SupportsAttachments), - (JsonRpcStrings.MultiConnectionProvider, capabilities.MultiConnectionProvider) - ]); - - _serializers[typeof(Artifact)] = new JsonObjectSerializer(artifact => - [ - (JsonRpcStrings.Uri, artifact.Uri), - (JsonRpcStrings.Producer, artifact.Producer), - (JsonRpcStrings.Type, artifact.Type), - (JsonRpcStrings.DisplayName, artifact.DisplayName), - (JsonRpcStrings.Description, artifact.Description) - ]); - - _serializers[typeof(DiscoverResponseArgs)] = new JsonObjectSerializer(response => []); - - _serializers[typeof(RunResponseArgs)] = new JsonObjectSerializer(response => - [ - (JsonRpcStrings.Attachments, response.Artifacts) - ]); - - _serializers[typeof(TestNodeUpdateMessage)] = new JsonObjectSerializer(message => - [ - (JsonRpcStrings.Node, message.TestNode), - (JsonRpcStrings.Parent, message.ParentTestNodeUid?.Value) - ]); - - _serializers[typeof(TestNodeStateChangedEventArgs)] = new JsonObjectSerializer(message => - [ - (JsonRpcStrings.RunId, message.RunId), - (JsonRpcStrings.Changes, message.Changes) - ]); - - _serializers[typeof(TestNode)] = new JsonObjectSerializer(message => - { - List<(string Name, object? Value)> properties = - [ - with(capacity: 16), - (JsonRpcStrings.Uid, message.Uid.Value), - (JsonRpcStrings.DisplayName, message.DisplayName) - ]; - - List>? traits = null; - bool hasActionNodeType = false; - - int attachmentIndex = 0; - foreach (IProperty property in message.Properties) - { - if (property is TestMetadataProperty metadataProperty) - { - (traits ??= []).Add(new KeyValuePair(metadataProperty.Key, metadataProperty.Value)); - continue; - } - - if (property is SerializableKeyValuePairStringProperty keyValuePairProperty) - { - properties.Add((keyValuePairProperty.Key, keyValuePairProperty.Value)); - continue; - } - - if (property is TestFileLocationProperty fileLocationProperty) - { - properties.Add(("location.file", fileLocationProperty.FilePath)); - properties.Add(("location.line-start", fileLocationProperty.LineSpan.Start.Line)); - properties.Add(("location.line-end", fileLocationProperty.LineSpan.End.Line)); - continue; - } - - if (property is TestMethodIdentifierProperty testMethodIdentifierProperty) - { - properties.Add(("location.type", RoslynString.IsNullOrEmpty(testMethodIdentifierProperty.Namespace) - ? testMethodIdentifierProperty.TypeName - : $"{testMethodIdentifierProperty.Namespace}.{testMethodIdentifierProperty.TypeName}")); - - properties.Add(("location.method", testMethodIdentifierProperty.ParameterTypeFullNames.Length > 0 - ? $"{testMethodIdentifierProperty.MethodName}({string.Join(',', testMethodIdentifierProperty.ParameterTypeFullNames)})" - : testMethodIdentifierProperty.MethodName)); - - properties.Add(("location.method-arity", testMethodIdentifierProperty.MethodArity)); - - continue; - } - - if (property is StandardOutputProperty standardOutputProperty) - { - properties.Add(("standardOutput", standardOutputProperty.StandardOutput)); - } - - if (property is StandardErrorProperty standardErrorProperty) - { - properties.Add(("standardError", standardErrorProperty.StandardError)); - } - - if (property is TestNodeStateProperty testNodeStateProperty) - { - properties.Add(("node-type", "action")); - hasActionNodeType = true; - switch (property) - { - case DiscoveredTestNodeStateProperty: - { - properties.Add(("execution-state", "discovered")); - break; - } - - case InProgressTestNodeStateProperty: - { - properties.Add(("execution-state", "in-progress")); - break; - } - - case PassedTestNodeStateProperty: - { - properties.Add(("execution-state", "passed")); - break; - } - - case SkippedTestNodeStateProperty skippedTestNodeStateProperty: - { - properties.Add(("execution-state", "skipped")); - if (!RoslynString.IsNullOrEmpty(skippedTestNodeStateProperty.Explanation)) - { - properties.Add(("error.message", skippedTestNodeStateProperty.Explanation)); - } - - break; - } - - case FailedTestNodeStateProperty failedTestNodeStateProperty: - { - properties.Add(("execution-state", "failed")); - Exception? exception = failedTestNodeStateProperty.Exception; - properties.Add(("error.message", failedTestNodeStateProperty.Explanation ?? exception?.Message)); - if (exception is not null) - { - properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); - properties.Add(("assert.actual", exception.Data["assert.actual"] ?? string.Empty)); - properties.Add(("assert.expected", exception.Data["assert.expected"] ?? string.Empty)); - } - - break; - } - - case TimeoutTestNodeStateProperty timeoutTestNodeStateProperty: - { - properties.Add(("execution-state", "timed-out")); - Exception? exception = timeoutTestNodeStateProperty.Exception; - properties.Add(("error.message", timeoutTestNodeStateProperty.Explanation ?? exception?.Message)); - if (exception is not null) - { - properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); - } - - break; - } - - case ErrorTestNodeStateProperty errorTestNodeStateProperty: - { - properties.Add(("execution-state", "error")); - Exception? exception = errorTestNodeStateProperty.Exception; - properties.Add(("error.message", errorTestNodeStateProperty.Explanation ?? exception?.Message)); - if (exception is not null) - { - properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); - } - - break; - } - -#pragma warning disable CS0618, MTP0001 // Type or member is obsolete - case CancelledTestNodeStateProperty canceledTestNodeStateProperty: -#pragma warning restore CS0618, MTP0001 // Type or member is obsolete - { - properties.Add(("execution-state", "canceled")); - Exception? exception = canceledTestNodeStateProperty.Exception; - properties.Add(("error.message", canceledTestNodeStateProperty.Explanation ?? exception?.Message)); - if (exception is not null) - { - properties.Add(("error.stacktrace", exception.StackTrace ?? string.Empty)); - } - - break; - } - - default: - throw new NotSupportedException($"Unsupported TestNodeStateProperty '{testNodeStateProperty.GetType()}'"); - } - - continue; - } - - if (property is TimingProperty timingProperty) - { - properties.Add(("time.duration-ms", timingProperty.GlobalTiming.Duration.TotalMilliseconds)); - continue; - } - - if (property is FileArtifactProperty artifact) - { - properties.Add(($"attachments.{attachmentIndex}.uri", artifact.FileInfo.FullName)); - properties.Add(($"attachments.{attachmentIndex}.display-name", artifact.DisplayName)); - properties.Add(($"attachments.{attachmentIndex}.description", artifact.Description)); - attachmentIndex++; - continue; - } - } - - if (traits is not null) - { - // Insert "traits" right after "uid" and "display-name" to preserve the - // original wire format ordering. - properties.Insert(2, ("traits", traits)); - } - - if (!hasActionNodeType) - { - properties.Add(("node-type", "group")); - } - - return [.. properties]; - }); - - _serializers[typeof(LogEventArgs)] = new JsonObjectSerializer(message => - [ - (JsonRpcStrings.Level, message.LogMessage.Level.ToString()), - (JsonRpcStrings.Message, message.LogMessage.Message) - ]); - - _serializers[typeof(CancelRequestArgs)] = new JsonObjectSerializer(request => - [ - (JsonRpcStrings.Id, request.CancelRequestId) - ]); - - _serializers[typeof(TelemetryEventArgs)] = new JsonObjectSerializer(ev => - [ - (JsonRpcStrings.EventName, ev.EventName), - (JsonRpcStrings.Metrics, ev.Metrics) - ]); - - _serializers[typeof(ProcessInfoArgs)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.Program, info.Program), - (JsonRpcStrings.Args, info.Args), - (JsonRpcStrings.WorkingDirectory, info.WorkingDirectory), - (JsonRpcStrings.EnvironmentVariables, info.EnvironmentVariables) - ]); - - _serializers[typeof(AttachDebuggerInfoArgs)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.ProcessId, info.ProcessId) - ]); - - _serializers[typeof(TestsAttachments)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.Attachments, info.Attachments) - ]); - - _serializers[typeof(RunTestAttachment)] = new JsonObjectSerializer(info => - [ - (JsonRpcStrings.Uri, info.Uri), - (JsonRpcStrings.Producer, info.Producer), - (JsonRpcStrings.Type, info.Type), - (JsonRpcStrings.DisplayName, info.DisplayName), - (JsonRpcStrings.Description, info.Description) - ]); - - // Serializers - _serializers[typeof(string)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v)); - _serializers[typeof(bool)] = new JsonValueSerializer((w, v) => w.WriteBooleanValue(v)); - _serializers[typeof(char)] = new JsonValueSerializer((w, v) => w.WriteStringValue($"{v}")); - _serializers[typeof(int)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(long)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(float)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(double)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(decimal)] = new JsonValueSerializer((w, v) => w.WriteNumberValue(v)); - _serializers[typeof(Guid)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v)); - _serializers[typeof(DateTime)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); - _serializers[typeof(DateTimeOffset)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); - - // _serializers[typeof(TimeSpan)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v.ToString())); // Remove for now - _serializers[typeof((string, object?)[])] = new JsonObjectSerializer<(string, object?)[]>(n => n); - _serializers[typeof(Dictionary)] = new JsonObjectSerializer>(d => [.. d.Select(kvp => (kvp.Key, (object?)kvp.Value))]); - - // Deserializers - _deserializers[typeof(string)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetString()!); - _deserializers[typeof(bool)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetBoolean()); - _deserializers[typeof(int)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetInt32()); - _deserializers[typeof(decimal)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetDecimal()); - _deserializers[typeof(DateTime)] = new JsonElementDeserializer((json, jsonDocument) => jsonDocument.GetDateTime()); - - _deserializers[typeof(IDictionary)] = new JsonElementDeserializer>((json, jsonDocument) => - { - Dictionary items = []; - foreach (JsonProperty kvp in jsonDocument.EnumerateObject()) - { - switch (kvp.Value.ValueKind) - { - case JsonValueKind.String: - items.Add(kvp.Name, kvp.Value.GetString()); - break; - case JsonValueKind.Number: - items.Add(kvp.Name, kvp.Value.GetInt32()); - break; - case JsonValueKind.True: - items.Add(kvp.Name, true); - break; - case JsonValueKind.False: - items.Add(kvp.Name, false); - break; - case JsonValueKind.Object: - items.Add(kvp.Name, json.Bind>(kvp.Value)); - break; - case JsonValueKind.Array: - items.Add(kvp.Name, json.Bind(kvp.Value)); - break; - case JsonValueKind.Null: - items.Add(kvp.Name, null); - break; - default: - throw new InvalidOperationException($"key: {kvp.Name}, value: {kvp.Value}, type: {kvp.Value.ValueKind}"); - } - } - - return items; - }); - - _deserializers[typeof(RpcMessage)] = new JsonElementDeserializer((json, jsonElement) => - { - ValidateJsonRpcHeader(json, jsonElement); - - if (json.TryBind(jsonElement, out string? method, JsonRpcStrings.Method)) - { - bool hasId = json.TryBind(jsonElement, out int id, JsonRpcStrings.Id); - - object? @params = null; - if (jsonElement.TryGetProperty(JsonRpcStrings.Params, out JsonElement value)) - { - try - { - // Parse the specific methods - @params = method switch - { - JsonRpcMethods.Initialize => json.Bind(value), - JsonRpcMethods.TestingDiscoverTests => json.Bind(value), - JsonRpcMethods.TestingRunTests => json.Bind(value), - JsonRpcMethods.CancelRequest => json.Bind(value), - JsonRpcMethods.Exit => json.Bind(value), - - // Note: Let the server report unknown RPC request back to the client. - _ => null, - }; - } - catch (Exception ex) when (ex is MessageFormatException or InvalidOperationException or JsonException) - { - // If params can't be deserialized for a request, capture the failure so - // we can later send back a properly coded JSON-RPC error using the request id. - // For notifications there's no one to respond to, but we still avoid - // crashing the message-handling loop by swallowing into the sentinel. - // We catch the broader set of deserialization-related exceptions because the - // request payload is untrusted client input and the lower-level helpers - // (e.g. JsonElement.GetString() on a non-string element) can throw types - // other than MessageFormatException. - @params = new InvalidRequestParamsArgs(ErrorCodes.InvalidParams, ex.Message); - } - } - - return hasId - ? new RequestMessage(id, method!, @params) - : new NotificationMessage(method!, @params); - } - - if (jsonElement.TryGetProperty(JsonRpcStrings.Result, out JsonElement element)) - { - // Note: Because the result message does not contain the original method name, - // it's not possible for us to do a typed deserialization. - // The best option we've got is to return a generic property bag. - int id = json.Bind(jsonElement, JsonRpcStrings.Id); - - IDictionary? result = element.ValueKind == JsonValueKind.Null ? null : - json.Bind>(jsonElement, JsonRpcStrings.Result); - - return new ResponseMessage(id, result); - } - - return json.TryBind(jsonElement, out ErrorMessage? errorMessage) ? errorMessage! : throw new MessageFormatException(); - }); - - _deserializers[typeof(InitializeRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => new InitializeRequestArgs( - ProcessId: json.Bind(jsonElement, JsonRpcStrings.ProcessId), - ClientInfo: json.Bind(jsonElement, JsonRpcStrings.ClientInfo), - Capabilities: json.Bind(jsonElement, JsonRpcStrings.Capabilities))); - - _deserializers[typeof(ClientInfo)] = new JsonElementDeserializer((json, jsonElement) => new ClientInfo( - Name: json.Bind(jsonElement, JsonRpcStrings.Name), - Version: json.Bind(jsonElement, JsonRpcStrings.Version))); - - _deserializers[typeof(ClientCapabilities)] = new JsonElementDeserializer((json, jsonElement) => - { - jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing); - - return new ClientCapabilities( - DebuggerProvider: json.Bind(testing, JsonRpcStrings.DebuggerProvider)); - }); - - _deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer( - (json, jsonElement) => new InitializeResponseArgs( - ProcessId: json.Bind(jsonElement, JsonRpcStrings.ProcessId), - ServerInfo: json.Bind(jsonElement, JsonRpcStrings.ServerInfo), - Capabilities: json.Bind(jsonElement, JsonRpcStrings.Capabilities))); - - _deserializers[typeof(ServerInfo)] = new JsonElementDeserializer( - (json, jsonElement) => new ServerInfo( - Name: json.Bind(jsonElement, JsonRpcStrings.Name), - Version: json.Bind(jsonElement, JsonRpcStrings.Version))); - - _deserializers[typeof(ServerCapabilities)] = new JsonElementDeserializer( - (json, jsonElement) => new ServerCapabilities( - TestingCapabilities: json.Bind(jsonElement, JsonRpcStrings.Testing))); - - _deserializers[typeof(ServerTestingCapabilities)] = new JsonElementDeserializer( - (json, jsonElement) => new ServerTestingCapabilities( - SupportsDiscovery: json.Bind(jsonElement, JsonRpcStrings.SupportsDiscovery), - MultiRequestSupport: json.Bind(jsonElement, JsonRpcStrings.MultiRequestSupport), - VSTestProviderSupport: json.Bind(jsonElement, JsonRpcStrings.VSTestProviderSupport), - SupportsAttachments: json.Bind(jsonElement, JsonRpcStrings.AttachmentsSupport), - MultiConnectionProvider: json.Bind(jsonElement, JsonRpcStrings.MultiConnectionProvider))); - - _deserializers[typeof(DiscoverRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => - { - string runId = json.Bind(jsonElement, JsonRpcStrings.RunId); - if (!Guid.TryParse(runId, out Guid result)) - { - throw new MessageFormatException(JsonRpcStrings.InvalidRunIdErrorMessage); - } - - json.TryArrayBind(jsonElement, out TestNode[]? testNodes, JsonRpcStrings.Tests); - json.TryBind(jsonElement, out string? graphFilter, JsonRpcStrings.Filter); - - return new DiscoverRequestArgs( - RunId: result, - TestNodes: testNodes, - GraphFilter: graphFilter); - }); - - _deserializers[typeof(RunRequestArgs)] = new JsonElementDeserializer((json, jsonElement) => - { - string runId = json.Bind(jsonElement, JsonRpcStrings.RunId); - if (!Guid.TryParse(runId, out Guid result)) - { - throw new MessageFormatException(JsonRpcStrings.InvalidRunIdErrorMessage); - } - - json.TryArrayBind(jsonElement, out TestNode[]? testNodes, JsonRpcStrings.Tests); - json.TryBind(jsonElement, out string? graphFilter, JsonRpcStrings.Filter); - - return new RunRequestArgs( - RunId: result, - TestNodes: testNodes, - GraphFilter: graphFilter); - }); - - _deserializers[typeof(TestNode)] = new JsonElementDeserializer( - (json, properties) => - { - PropertyBag propertyBag = new(); - string uid = json.Bind(properties, JsonRpcStrings.Uid) ?? string.Empty; - string displayName = json.Bind(properties, JsonRpcStrings.DisplayName); - - if (json.TryBind(properties, out string? locationFile, "location.file")) - { - json.TryBind(properties, out int locationLineStart, "location.line-start"); - json.TryBind(properties, out int locationLineEnd, "location.line-end"); - - TestFileLocationProperty testFileLocationProperty = new( - locationFile!, - new LinePositionSpan(new LinePosition(locationLineStart, 0), new LinePosition(locationLineEnd, 0))); - propertyBag.Add(testFileLocationProperty); - } - - return new TestNode - { - Uid = new TestNodeUid(uid), - DisplayName = displayName, - Properties = propertyBag, - }; - }); - - _deserializers[typeof(CancelRequestArgs)] = new JsonElementDeserializer( - (json, jsonElement) => json.TryBind(jsonElement, out int id, JsonRpcStrings.Id) ? new CancelRequestArgs(id) : throw new MessageFormatException("id field should be an int")); - - _deserializers[typeof(ExitRequestArgs)] = new JsonElementDeserializer( - (json, jsonElement) => new ExitRequestArgs()); - - _deserializers[typeof(ErrorMessage)] = new JsonElementDeserializer( - (json, jsonElement) => - { - ValidateJsonRpcHeader(json, jsonElement); - - int id = json.Bind(jsonElement, JsonRpcStrings.Id); - JsonElement error = jsonElement.GetProperty(JsonRpcStrings.Error); - - int code = json.Bind(error, JsonRpcStrings.Code); - string message = json.Bind(error, JsonRpcStrings.Message); - - if (json.TryBind(error, out IDictionary? data, JsonRpcStrings.Data) && data?.Count == 0) - { - data = null; - } - - return new ErrorMessage( - Id: id, - ErrorCode: code, - Message: message ?? string.Empty, - Data: data); - }); + RegisterDefaultSerializers(_serializers); + RegisterDefaultDeserializers(_deserializers); // Try to add serializers passed from outside if (serializers is not null) From 9f5633122448992dc8d7d02729d9c65a53decfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 11 Jun 2026 15:20:41 +0200 Subject: [PATCH 2/3] Address PR review: fix RpcMessage result binding + stale comment - Json.Deserializers.cs: bind ResponseMessage.Result to IDictionary to match the registered deserializer. The previous IDictionary binding would throw InvalidOperationException("Cannot find deserializer for ...") for any non-null result payload because Deserialize performs an exact typeof(T) lookup. Pre-existing bug carried over from the unsplit Json.cs but fixed here as part of the review. - Json.Serializers.cs: refresh the commented-out TimeSpan serializer line to reference the 'serializers' parameter instead of the old '_serializers' field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ServerMode/JsonRpc/Json/Json.Deserializers.cs | 4 ++-- .../ServerMode/JsonRpc/Json/Json.Serializers.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs index 537b418899..55e3f182d1 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Deserializers.cs @@ -106,8 +106,8 @@ private static void RegisterDefaultDeserializers(Dictionary(jsonElement, JsonRpcStrings.Id); - IDictionary? result = element.ValueKind == JsonValueKind.Null ? null : - json.Bind>(jsonElement, JsonRpcStrings.Result); + IDictionary? result = element.ValueKind == JsonValueKind.Null ? null : + json.Bind>(jsonElement, JsonRpcStrings.Result); return new ResponseMessage(id, result); } diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs index d4c7fa1598..367987c8ac 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.Serializers.cs @@ -166,7 +166,7 @@ private static void RegisterDefaultSerializers(Dictionary serializers[typeof(DateTime)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); serializers[typeof(DateTimeOffset)] = new JsonValueSerializer((w, v) => w.WriteRawValue($"\"{v:o}\"", skipInputValidation: true)); - // _serializers[typeof(TimeSpan)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v.ToString())); // Remove for now + // serializers[typeof(TimeSpan)] = new JsonValueSerializer((w, v) => w.WriteStringValue(v.ToString())); // Remove for now serializers[typeof((string, object?)[])] = new JsonObjectSerializer<(string, object?)[]>(n => n); serializers[typeof(Dictionary)] = new JsonObjectSerializer>(d => [.. d.Select(kvp => (kvp.Key, (object?)kvp.Value))]); } From 15904bbbaa8337ade0953bafbe29ffb545fa3a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 11 Jun 2026 16:52:48 +0200 Subject: [PATCH 3/3] Trigger CI retry: flaky ILLink Mono.Cecil PDB crash on net10.0 trim test Windows Debug job failed with an access violation (0xC0000005) inside Mono.Cecil.Pdb.ISymUnmanagedWriter2.Close() while ILLink wrote the trim publish output for MSTestTrimAnalysisTest_net10.0. The crash is a known flake in the bundled microsoft.net.illink.tasks 10.0.7 native PDB writer on Windows and is unrelated to this PR's Json.cs split refactor. No code change; pushing an empty commit to retrigger the failed job. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>