diff --git a/src/Platform/Microsoft.Testing.Platform/IPC/Serializers/RegisterSerializers.cs b/src/Platform/Microsoft.Testing.Platform/IPC/Serializers/RegisterSerializers.cs index d7d1710874..3fc7d6cfe0 100644 --- a/src/Platform/Microsoft.Testing.Platform/IPC/Serializers/RegisterSerializers.cs +++ b/src/Platform/Microsoft.Testing.Platform/IPC/Serializers/RegisterSerializers.cs @@ -13,12 +13,13 @@ namespace Microsoft.Testing.Platform.IPC.Serializers; * TestHostCompletedRequestSerializer: 1 * TestHostProcessPIDRequestSerializer: 2 * CommandLineOptionMessagesSerializer: 3 - * ModuleSerializer: 4 - * DiscoveredTestMessageSerializer: 5 - * TestResultMessageSerializer: 6 - * FileArtifactMessageSerializer: 7 + * (4 is reserved - previously used by a removed serializer) + * DiscoveredTestMessagesSerializer: 5 + * TestResultMessagesSerializer: 6 + * FileArtifactMessagesSerializer: 7 * TestSessionEventSerializer: 8 * HandshakeMessageSerializer: 9 + * TestInProgressMessagesSerializer: 10 */ [Embedded] @@ -35,5 +36,6 @@ public static void RegisterAllSerializers(this NamedPipeBase namedPipeBase) namedPipeBase.RegisterSerializer(new FileArtifactMessagesSerializer(), typeof(FileArtifactMessages)); namedPipeBase.RegisterSerializer(new TestSessionEventSerializer(), typeof(TestSessionEvent)); namedPipeBase.RegisterSerializer(new HandshakeMessageSerializer(), typeof(HandshakeMessage)); + namedPipeBase.RegisterSerializer(new TestInProgressMessagesSerializer(), typeof(TestInProgressMessages)); } } diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/DotnetTestDataConsumer.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/DotnetTestDataConsumer.cs index cec7a46740..76719b5321 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/DotnetTestDataConsumer.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/DotnetTestDataConsumer.cs @@ -107,6 +107,21 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella await _dotnetTestConnection.SendMessageAsync(testResultMessages).ConfigureAwait(false); break; + case TestStates.InProgress: + // Non-IDE consumers (e.g. `dotnet test` with MTP) render in-progress events as a + // separate "currently running tests" panel; they don't expect them as TestResultMessages. + TestInProgressMessages inProgressMessages = new( + ExecutionId, + DotnetTestConnection.InstanceId, + [ + new TestInProgressMessage( + testNodeUpdateMessage.TestNode.Uid.Value, + testNodeUpdateMessage.TestNode.DisplayName), + ]); + + await _dotnetTestConnection.SendMessageAsync(inProgressMessages).ConfigureAwait(false); + break; + case TestStates.Failed: case TestStates.Error: case TestStates.Timeout: diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Models/TestInProgressMessages.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Models/TestInProgressMessages.cs new file mode 100644 index 0000000000..c7b9937841 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Models/TestInProgressMessages.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.IPC.Models; + +internal sealed record TestInProgressMessage(string? Uid, string? DisplayName); + +internal sealed record TestInProgressMessages(string? ExecutionId, string? InstanceId, TestInProgressMessage[] InProgressMessages) : IRequest; diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/ObjectFieldIds.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/ObjectFieldIds.cs index 84c5dfe259..f328182ffa 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/ObjectFieldIds.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/ObjectFieldIds.cs @@ -141,3 +141,18 @@ internal static class HandshakeMessageFieldsId { public const int MessagesSerializerId = 9; } + +internal static class TestInProgressMessagesFieldsId +{ + public const int MessagesSerializerId = 10; + + public const ushort ExecutionId = 1; + public const ushort InstanceId = 2; + public const ushort TestInProgressMessageList = 3; +} + +internal static class TestInProgressMessageFieldsId +{ + public const ushort Uid = 1; + public const ushort DisplayName = 2; +} diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Serializers/TestInProgressMessagesSerializer.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Serializers/TestInProgressMessagesSerializer.cs new file mode 100644 index 0000000000..2f88f8f9d1 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Serializers/TestInProgressMessagesSerializer.cs @@ -0,0 +1,161 @@ +// 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.IPC.Models; + +namespace Microsoft.Testing.Platform.IPC.Serializers; + +/* + |---FieldCount---| 2 bytes + + |---ExecutionId Id---| (2 bytes) + |---ExecutionId Size---| (4 bytes) + |---ExecutionId Value---| (n bytes) + + |---InstanceId Id---| (2 bytes) + |---InstanceId Size---| (4 bytes) + |---InstanceId Value---| (n bytes) + + |---TestInProgressMessageList Id---| (2 bytes) + |---TestInProgressMessageList Size---| (4 bytes) + |---TestInProgressMessageList Value---| (n bytes) + |---TestInProgressMessageList Length---| (4 bytes) + + |---TestInProgressMessageList[0] FieldCount---| 2 bytes + + |---TestInProgressMessageList[0].Uid Id---| (2 bytes) + |---TestInProgressMessageList[0].Uid Size---| (4 bytes) + |---TestInProgressMessageList[0].Uid Value---| (n bytes) + + |---TestInProgressMessageList[0].DisplayName Id---| (2 bytes) + |---TestInProgressMessageList[0].DisplayName Size---| (4 bytes) + |---TestInProgressMessageList[0].DisplayName Value---| (n bytes) +*/ + +internal sealed class TestInProgressMessagesSerializer : NamedPipeSerializer, INamedPipeSerializer +{ + public override int Id => TestInProgressMessagesFieldsId.MessagesSerializerId; + + protected override TestInProgressMessages DeserializeCore(Stream stream) + { + string? executionId = null; + string? instanceId = null; + TestInProgressMessage[]? inProgressMessages = []; + + ushort fieldCount = ReadUShort(stream); + + for (int i = 0; i < fieldCount; i++) + { + int fieldId = ReadUShort(stream); + int fieldSize = ReadInt(stream); + + switch (fieldId) + { + case TestInProgressMessagesFieldsId.ExecutionId: + executionId = ReadStringValue(stream, fieldSize); + break; + + case TestInProgressMessagesFieldsId.InstanceId: + instanceId = ReadStringValue(stream, fieldSize); + break; + + case TestInProgressMessagesFieldsId.TestInProgressMessageList: + inProgressMessages = ReadInProgressMessagesPayload(stream); + break; + + default: + // If we don't recognize the field id, skip the payload corresponding to that field + SetPosition(stream, stream.Position + fieldSize); + break; + } + } + + return new(executionId, instanceId, inProgressMessages); + } + + private static TestInProgressMessage[] ReadInProgressMessagesPayload(Stream stream) + { + int length = ReadInt(stream); + var inProgressMessages = new TestInProgressMessage[length]; + for (int i = 0; i < length; i++) + { + string? uid = null; + string? displayName = null; + + int fieldCount = ReadUShort(stream); + + for (int j = 0; j < fieldCount; j++) + { + int fieldId = ReadUShort(stream); + int fieldSize = ReadInt(stream); + + switch (fieldId) + { + case TestInProgressMessageFieldsId.Uid: + uid = ReadStringValue(stream, fieldSize); + break; + + case TestInProgressMessageFieldsId.DisplayName: + displayName = ReadStringValue(stream, fieldSize); + break; + + default: + SetPosition(stream, stream.Position + fieldSize); + break; + } + } + + inProgressMessages[i] = new TestInProgressMessage(uid, displayName); + } + + return inProgressMessages; + } + + protected override void SerializeCore(TestInProgressMessages objectToSerialize, Stream stream) + { + RoslynDebug.Assert(stream.CanSeek, "We expect a seekable stream."); + + WriteUShort(stream, GetFieldCount(objectToSerialize)); + + WriteField(stream, TestInProgressMessagesFieldsId.ExecutionId, objectToSerialize.ExecutionId); + WriteField(stream, TestInProgressMessagesFieldsId.InstanceId, objectToSerialize.InstanceId); + WriteInProgressMessagesPayload(stream, objectToSerialize.InProgressMessages); + } + + private static void WriteInProgressMessagesPayload(Stream stream, TestInProgressMessage[]? inProgressMessageList) + { + if (inProgressMessageList is null || inProgressMessageList.Length == 0) + { + return; + } + + WriteUShort(stream, TestInProgressMessagesFieldsId.TestInProgressMessageList); + + // We will reserve an int (4 bytes) + // so that we fill the size later, once we write the payload + WriteInt(stream, 0); + + long before = stream.Position; + WriteInt(stream, inProgressMessageList.Length); + foreach (TestInProgressMessage inProgressMessage in inProgressMessageList) + { + WriteUShort(stream, GetFieldCount(inProgressMessage)); + + WriteField(stream, TestInProgressMessageFieldsId.Uid, inProgressMessage.Uid); + WriteField(stream, TestInProgressMessageFieldsId.DisplayName, inProgressMessage.DisplayName); + } + + // NOTE: We are able to seek only if we are using a MemoryStream + // thus, the seek operation is fast as we are only changing the value of a property + WriteAtPosition(stream, (int)(stream.Position - before), before - sizeof(int)); + } + + private static ushort GetFieldCount(TestInProgressMessages inProgressMessages) => + (ushort)((inProgressMessages.ExecutionId is null ? 0 : 1) + + (inProgressMessages.InstanceId is null ? 0 : 1) + + (IsNullOrEmpty(inProgressMessages.InProgressMessages) ? 0 : 1)); + + private static ushort GetFieldCount(TestInProgressMessage inProgressMessage) => + (ushort)((inProgressMessage.Uid is null ? 0 : 1) + + (inProgressMessage.DisplayName is null ? 0 : 1)); +} diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/ProtocolTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/ProtocolTests.cs index 6486326e85..735b8f2ddd 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/ProtocolTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/ProtocolTests.cs @@ -150,6 +150,53 @@ public void HandshakeMessageWithNullProperties() Assert.IsEmpty(actual.Properties); } + [TestMethod] + public void TestInProgressMessagesSerializeDeserialize() + { + object serializer = new TestInProgressMessagesSerializer(); + + var message = new TestInProgressMessages( + "MyExecId", + "MyInstId", + [ + new TestInProgressMessage("uid-1", "Display 1"), + new TestInProgressMessage("uid-2", null), + new TestInProgressMessage(null, "Display 3"), + ]); + + var stream = new MemoryStream(); + Serialize(serializer, message, stream); + stream.Seek(0, SeekOrigin.Begin); + var actual = (TestInProgressMessages)Deserialize(serializer, stream); + + Assert.AreEqual(message.ExecutionId, actual.ExecutionId); + Assert.AreEqual(message.InstanceId, actual.InstanceId); + Assert.HasCount(message.InProgressMessages.Length, actual.InProgressMessages); + for (int i = 0; i < message.InProgressMessages.Length; i++) + { + Assert.AreEqual(message.InProgressMessages[i].Uid, actual.InProgressMessages[i].Uid); + Assert.AreEqual(message.InProgressMessages[i].DisplayName, actual.InProgressMessages[i].DisplayName); + } + } + + [TestMethod] + public void TestInProgressMessagesSerializeDeserialize_EmptyList() + { + object serializer = new TestInProgressMessagesSerializer(); + + var message = new TestInProgressMessages("execId", "instId", []); + + var stream = new MemoryStream(); + Serialize(serializer, message, stream); + stream.Seek(0, SeekOrigin.Begin); + var actual = (TestInProgressMessages)Deserialize(serializer, stream); + + Assert.AreEqual(message.ExecutionId, actual.ExecutionId); + Assert.AreEqual(message.InstanceId, actual.InstanceId); + Assert.IsNotNull(actual.InProgressMessages); + Assert.HasCount(0, actual.InProgressMessages); + } + private static void Serialize(object serializer, TMessage message, Stream stream) => serializer.GetType() .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)