diff --git a/src/Cli/dotnet/commands/dotnet-test/CliConstants.cs b/src/Cli/dotnet/commands/dotnet-test/CliConstants.cs index 818b85449a55..1b1355b164cb 100644 --- a/src/Cli/dotnet/commands/dotnet-test/CliConstants.cs +++ b/src/Cli/dotnet/commands/dotnet-test/CliConstants.cs @@ -16,6 +16,42 @@ internal static class CliConstants public const string ServerOptionValue = "dotnettestcli"; public const string MSBuildExeName = "MSBuild.dll"; + } + + internal static class TestStates + { + internal const string Passed = "Passed"; + + internal const string Skipped = "Skipped"; + + internal const string Failed = "Failed"; + + internal const string Error = "Error"; + + internal const string Timeout = "Timeout"; + + internal const string Cancelled = "Cancelled"; + } + + internal static class SessionEventTypes + { + internal const string TestSessionStart = "TestSessionStart"; + internal const string TestSessionEnd = "TestSessionEnd"; + } + internal static class HandshakeInfoPropertyNames + { + internal const string PID = "PID"; + internal const string Architecture = "Architecture"; + internal const string Framework = "Framework"; + internal const string OS = "OS"; + internal const string ProtocolVersion = "ProtocolVersion"; + internal const string HostType = "HostType"; + internal const string ModulePath = "ModulePath"; + } + + internal static class ProtocolConstants + { + internal const string Version = "1.0.0"; } } diff --git a/src/Cli/dotnet/commands/dotnet-test/CustomEventArgs.cs b/src/Cli/dotnet/commands/dotnet-test/CustomEventArgs.cs index fa3b9abddfce..35f19decb0cd 100644 --- a/src/Cli/dotnet/commands/dotnet-test/CustomEventArgs.cs +++ b/src/Cli/dotnet/commands/dotnet-test/CustomEventArgs.cs @@ -5,13 +5,45 @@ namespace Microsoft.DotNet.Cli { - internal class ErrorEventArgs : EventArgs + internal class HandshakeInfoArgs : EventArgs { - public string ErrorMessage { get; set; } + public HandshakeInfo handshakeInfo { get; set; } } internal class HelpEventArgs : EventArgs { public CommandLineOptionMessages CommandLineOptionMessages { get; set; } } + + internal class SuccessfulTestResultEventArgs : EventArgs + { + public SuccessfulTestResultMessage SuccessfulTestResultMessage { get; set; } + } + + internal class FailedTestResultEventArgs : EventArgs + { + public FailedTestResultMessage FailedTestResultMessage { get; set; } + } + + internal class FileArtifactInfoEventArgs : EventArgs + { + public FileArtifactInfo FileArtifactInfo { get; set; } + } + + internal class SessionEventArgs : EventArgs + { + public TestSessionEvent SessionEvent { get; set; } + } + + internal class ErrorEventArgs : EventArgs + { + public string ErrorMessage { get; set; } + } + + internal class TestProcessExitEventArgs : EventArgs + { + public List OutputData { get; set; } + public List ErrorData { get; set; } + public int ExitCode { get; set; } + } } diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/CommandLineOptionMessages.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/CommandLineOptionMessages.cs index f0fcae78be6c..da147078e075 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/CommandLineOptionMessages.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/CommandLineOptionMessages.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + namespace Microsoft.DotNet.Tools.Test { - internal sealed record CommandLineOptionMessage(string Name, string Description, bool IsHidden, bool IsBuiltIn) : IRequest; + internal sealed record CommandLineOptionMessage(string? Name, string? Description, bool? IsHidden, bool? IsBuiltIn) : IRequest; - internal sealed record CommandLineOptionMessages(string ModulePath, CommandLineOptionMessage[] CommandLineOptionMessageList) : IRequest; + internal sealed record CommandLineOptionMessages(string? ModulePath, CommandLineOptionMessage[]? CommandLineOptionMessageList) : IRequest; } diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/FileArtifactInfo.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/FileArtifactInfo.cs new file mode 100644 index 000000000000..c2d892d3d733 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/FileArtifactInfo.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace Microsoft.DotNet.Tools.Test +{ + internal sealed record FileArtifactInfo(string? FullPath, string? DisplayName, string? Description, string? TestUid, string? TestDisplayName, string? SessionUid, string? ModulePath) : IRequest; +} diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/HandshakeInfo.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/HandshakeInfo.cs new file mode 100644 index 000000000000..f33d462b8a51 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/HandshakeInfo.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace Microsoft.DotNet.Tools.Test +{ + internal sealed record HandshakeInfo(Dictionary? Properties) : IRequest, IResponse; +} diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/Module.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/Module.cs index 7e9689b05dd1..568df5c66e58 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/Module.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/Module.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + namespace Microsoft.DotNet.Tools.Test; -internal sealed record class Module(string DLLPath, string ProjectPath) : IRequest; +internal sealed record class Module(string? DLLPath, string? ProjectPath) : IRequest; diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestResultMessage.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestResultMessage.cs new file mode 100644 index 000000000000..2cedcf7bcfec --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestResultMessage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace Microsoft.DotNet.Tools.Test +{ + internal sealed record SuccessfulTestResultMessage(string? Uid, string? DisplayName, string? State, string? Reason, string? SessionUid, string? ModulePath) : IRequest; + + internal sealed record FailedTestResultMessage(string? Uid, string? DisplayName, string? State, string? Reason, string? ErrorMessage, string? ErrorStackTrace, string? SessionUid, string? ModulePath) : IRequest; +} diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestSessionEvent.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestSessionEvent.cs new file mode 100644 index 000000000000..02b96daa941f --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestSessionEvent.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace Microsoft.DotNet.Tools.Test +{ + internal sealed record TestSessionEvent(string? SessionType, string? SessionUid, string? ModulePath) : IRequest; +} diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/ObjectFieldIds.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/ObjectFieldIds.cs index 1e56428cdf67..8b0cfb0fdc40 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/ObjectFieldIds.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/ObjectFieldIds.cs @@ -20,4 +20,44 @@ internal static class CommandLineOptionMessageFieldsId internal const int IsHidden = 3; internal const int IsBuiltIn = 4; } + + internal static class SuccessfulTestResultMessageFieldsId + { + internal const int Uid = 1; + internal const int DisplayName = 2; + internal const int State = 3; + internal const int Reason = 4; + internal const int SessionUid = 5; + internal const int ModulePath = 6; + } + + internal static class FailedTestResultMessageFieldsId + { + internal const int Uid = 1; + internal const int DisplayName = 2; + internal const int State = 3; + internal const int Reason = 4; + internal const int ErrorMessage = 5; + internal const int ErrorStackTrace = 6; + internal const int SessionUid = 7; + internal const int ModulePath = 8; + } + + internal static class FileArtifactInfoFieldsId + { + internal const int FullPath = 1; + internal const int DisplayName = 2; + internal const int Description = 3; + internal const int TestUid = 4; + internal const int TestDisplayName = 5; + internal const int SessionUid = 6; + internal const int ModulePath = 7; + } + + internal static class TestSessionEventFieldsId + { + internal const int SessionType = 1; + internal const int SessionUid = 2; + internal const int ModulePath = 3; + } } diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/BaseSerializer.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/BaseSerializer.cs index 652761727855..8cfea9420cc5 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/BaseSerializer.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/BaseSerializer.cs @@ -228,11 +228,26 @@ protected static void WriteField(Stream stream, ushort id, string? value) WriteString(stream, value); } - protected static void WriteField(Stream stream, ushort id, bool value) + protected static void WriteField(Stream stream, string? value) { + if (value is null) + { + return; + } + + WriteString(stream, value); + } + + protected static void WriteField(Stream stream, ushort id, bool? value) + { + if (value is null) + { + return; + } + WriteShort(stream, id); WriteSize(stream); - WriteBool(stream, value); + WriteBool(stream, value.Value); } protected static void SetPosition(Stream stream, long position) => stream.Position = position; diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/CommandLineOptionMessagesSerializer.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/CommandLineOptionMessagesSerializer.cs index 41d4c97569c3..fe1e1024bf56 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/CommandLineOptionMessagesSerializer.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/CommandLineOptionMessagesSerializer.cs @@ -46,7 +46,7 @@ internal sealed class CommandLineOptionMessagesSerializer : BaseSerializer, INam public object Deserialize(Stream stream) { - string moduleName = string.Empty; + string? moduleName = null; List? commandLineOptionMessages = null; ushort fieldCount = ReadShort(stream); @@ -83,8 +83,8 @@ private static List ReadCommandLineOptionMessagesPaylo int length = ReadInt(stream); for (int i = 0; i < length; i++) { - string name = string.Empty, description = string.Empty, arity = string.Empty; - bool isHidden = false, isBuiltIn = false; + string? name = null, description = null; + bool? isHidden = null, isBuiltIn = null; int fieldCount = ReadShort(stream); @@ -135,9 +135,9 @@ public void Serialize(object objectToSerialize, Stream stream) WriteCommandLineOptionMessagesPayload(stream, commandLineOptionMessages.CommandLineOptionMessageList); } - private static void WriteCommandLineOptionMessagesPayload(Stream stream, CommandLineOptionMessage[] commandLineOptionMessageList) + private static void WriteCommandLineOptionMessagesPayload(Stream stream, CommandLineOptionMessage[]? commandLineOptionMessageList) { - if (IsNull(commandLineOptionMessageList)) + if (commandLineOptionMessageList is null || commandLineOptionMessageList.Length == 0) { return; } @@ -165,13 +165,13 @@ private static void WriteCommandLineOptionMessagesPayload(Stream stream, Command WriteAtPosition(stream, (int)(stream.Position - before), before - sizeof(int)); } - private static ushort GetFieldCount(CommandLineOptionMessages commandLineOptionMessages) => (ushort)((string.IsNullOrEmpty(commandLineOptionMessages.ModulePath) ? 0 : 1) + - (commandLineOptionMessages is null ? 0 : 1)); + private static ushort GetFieldCount(CommandLineOptionMessages commandLineOptionMessages) => + (ushort)((commandLineOptionMessages.ModulePath is null ? 0 : 1) + + (commandLineOptionMessages is null ? 0 : 1)); - private static ushort GetFieldCount(CommandLineOptionMessage commandLineOptionMessage) => (ushort)((string.IsNullOrEmpty(commandLineOptionMessage.Name) ? 0 : 1) + - (string.IsNullOrEmpty(commandLineOptionMessage.Description) ? 0 : 1) + - 2); - - private static bool IsNull(T[] items) => items is null || items.Length == 0; + private static ushort GetFieldCount(CommandLineOptionMessage commandLineOptionMessage) => + (ushort)((commandLineOptionMessage.Name is null ? 0 : 1) + + (commandLineOptionMessage.Description is null ? 0 : 1) + + 2); } } diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/FileArtifactInfoSerializer.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/FileArtifactInfoSerializer.cs new file mode 100644 index 000000000000..7865a9a28c8d --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/FileArtifactInfoSerializer.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System.Diagnostics; + +namespace Microsoft.DotNet.Tools.Test +{ + /* + |---FieldCount---| 2 bytes + + |---File FullPath Id---| 1 (2 bytes) + |---File FullPath Size---| (4 bytes) + |---File FullPath Value---| (n bytes) + + |---File DisplayName Id---| 1 (2 bytes) + |---File DisplayName Size---| (4 bytes) + |---File DisplayName Value---| (n bytes) + + |---File Description Id---| 1 (2 bytes) + |---File Description Size---| (4 bytes) + |---File Description Value---| (n bytes) + + |---File TestUid Id---| 1 (2 bytes) + |---File TestUid Size---| (4 bytes) + |---File TestUid Value---| (n bytes) + + |---File TestDisplayName Id---| 1 (2 bytes) + |---File TestDisplayName Size---| (4 bytes) + |---File TestDisplayName Value---| (n bytes) + + |---File SessionUid Id---| 1 (2 bytes) + |---File SessionUid Size---| (4 bytes) + |---File SessionUid Value---| (n bytes) + + |---File ModulePath Id---| 1 (2 bytes) + |---File ModulePath Size---| (4 bytes) + |---File ModulePath Value---| (n bytes) + */ + + internal sealed class FileArtifactInfoSerializer : BaseSerializer, INamedPipeSerializer + { + public int Id => 7; + + public object Deserialize(Stream stream) + { + string? fullPath = null; + string? displayName = null; + string? description = null; + string? testUid = null; + string? testDisplayName = null; + string? sessionUid = null; + string? modulePath = null; + + ushort fieldCount = ReadShort(stream); + + for (int i = 0; i < fieldCount; i++) + { + int fieldId = ReadShort(stream); + int fieldSize = ReadInt(stream); + + switch (fieldId) + { + case FileArtifactInfoFieldsId.FullPath: + fullPath = ReadString(stream); + break; + + case FileArtifactInfoFieldsId.DisplayName: + displayName = ReadString(stream); + break; + + case FileArtifactInfoFieldsId.Description: + description = ReadString(stream); + break; + + case FileArtifactInfoFieldsId.TestUid: + testUid = ReadString(stream); + break; + + case FileArtifactInfoFieldsId.TestDisplayName: + testDisplayName = ReadString(stream); + break; + + case FileArtifactInfoFieldsId.SessionUid: + sessionUid = ReadString(stream); + break; + + case FileArtifactInfoFieldsId.ModulePath: + modulePath = ReadString(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 FileArtifactInfo(fullPath, displayName, description, testUid, testDisplayName, sessionUid, modulePath); + } + + public void Serialize(object objectToSerialize, Stream stream) + { + Debug.Assert(stream.CanSeek, "We expect a seekable stream."); + + var fileArtifactInfo = (FileArtifactInfo)objectToSerialize; + + WriteShort(stream, GetFieldCount(fileArtifactInfo)); + + WriteField(stream, FileArtifactInfoFieldsId.FullPath, fileArtifactInfo.FullPath); + WriteField(stream, FileArtifactInfoFieldsId.DisplayName, fileArtifactInfo.DisplayName); + WriteField(stream, FileArtifactInfoFieldsId.Description, fileArtifactInfo.Description); + WriteField(stream, FileArtifactInfoFieldsId.TestUid, fileArtifactInfo.TestUid); + WriteField(stream, FileArtifactInfoFieldsId.TestDisplayName, fileArtifactInfo.TestDisplayName); + WriteField(stream, FileArtifactInfoFieldsId.SessionUid, fileArtifactInfo.SessionUid); + WriteField(stream, FileArtifactInfoFieldsId.ModulePath, fileArtifactInfo.ModulePath); + } + + private static ushort GetFieldCount(FileArtifactInfo fileArtifactInfo) => + (ushort)((fileArtifactInfo.FullPath is null ? 0 : 1) + + (fileArtifactInfo.DisplayName is null ? 0 : 1) + + (fileArtifactInfo.Description is null ? 0 : 1) + + (fileArtifactInfo.TestUid is null ? 0 : 1) + + (fileArtifactInfo.TestDisplayName is null ? 0 : 1) + + (fileArtifactInfo.SessionUid is null ? 0 : 1) + + (fileArtifactInfo.ModulePath is null ? 0 : 1)); + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/HandshakeInfoSerializer.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/HandshakeInfoSerializer.cs new file mode 100644 index 000000000000..f331994b08b9 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/HandshakeInfoSerializer.cs @@ -0,0 +1,46 @@ +// 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.Diagnostics; + +namespace Microsoft.DotNet.Tools.Test +{ + internal sealed class HandshakeInfoSerializer : BaseSerializer, INamedPipeSerializer + { + public int Id => 9; + + public object Deserialize(Stream stream) + { + Dictionary properties = new(); + + ushort fieldCount = ReadShort(stream); + + for (int i = 0; i < fieldCount; i++) + { + properties.Add(ReadString(stream), ReadString(stream)); + } + + return new HandshakeInfo(properties); + } + + public void Serialize(object objectToSerialize, Stream stream) + { + Debug.Assert(stream.CanSeek, "We expect a seekable stream."); + + var handshakeInfo = (HandshakeInfo)objectToSerialize; + + if (handshakeInfo.Properties is null || handshakeInfo.Properties.Count == 0) + { + return; + } + + WriteShort(stream, (ushort)handshakeInfo.Properties.Count); + foreach (KeyValuePair property in handshakeInfo.Properties) + { + WriteField(stream, property.Key); + WriteField(stream, property.Value); + } + } + } + +} diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/RegisterSerializers.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/RegisterSerializers.cs index b38aca92969c..e5eb8e5278df 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/RegisterSerializers.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/RegisterSerializers.cs @@ -11,7 +11,13 @@ namespace Microsoft.DotNet.Tools.Test; * TestHostProcessPIDRequestSerializer: 2 * CommandLineOptionMessagesSerializer: 3 * ModuleSerializer: 4 -*/ + * SuccessfulTestResultMessageSerializer: 5 + * FailedTestResultMessageSerializer: 6 + * FileArtifactInfoSerializer: 7 + * TestSessionEventSerializer: 8 + * HandshakeInfoSerializer: 9 + */ + internal static class RegisterSerializers { public static void RegisterAllSerializers(this NamedPipeBase namedPipeBase) @@ -19,5 +25,10 @@ public static void RegisterAllSerializers(this NamedPipeBase namedPipeBase) namedPipeBase.RegisterSerializer(new VoidResponseSerializer(), typeof(VoidResponse)); namedPipeBase.RegisterSerializer(new ModuleSerializer(), typeof(Module)); namedPipeBase.RegisterSerializer(new CommandLineOptionMessagesSerializer(), typeof(CommandLineOptionMessages)); + namedPipeBase.RegisterSerializer(new SuccessfulTestResultMessageSerializer(), typeof(SuccessfulTestResultMessage)); + namedPipeBase.RegisterSerializer(new FailedTestResultMessageSerializer(), typeof(FailedTestResultMessage)); + namedPipeBase.RegisterSerializer(new FileArtifactInfoSerializer(), typeof(FileArtifactInfo)); + namedPipeBase.RegisterSerializer(new TestSessionEventSerializer(), typeof(TestSessionEvent)); + namedPipeBase.RegisterSerializer(new HandshakeInfoSerializer(), typeof(HandshakeInfo)); } } diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestResultMessageSerializer.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestResultMessageSerializer.cs new file mode 100644 index 000000000000..2ea0d09b5181 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestResultMessageSerializer.cs @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System.Diagnostics; + +namespace Microsoft.DotNet.Tools.Test +{ + /* + |---FieldCount---| 2 bytes + + |---Test Uid Id---| 1 (2 bytes) + |---Test Uid Size---| (4 bytes) + |---Test Uid Value---| (n bytes) + + |---Test DisplayName Id---| 1 (2 bytes) + |---Test DisplayName Size---| (4 bytes) + |---Test DisplayName Value---| (n bytes) + + |---Test State Id---| 1 (2 bytes) + |---Test State Size---| (4 bytes) + |---Test State Value---| (n bytes) + + |---Test Reason Id---| 1 (2 bytes) + |---Test Reason Size---| (4 bytes) + |---Test Reason Value---| (n bytes) + + |---Test SessionUid Id---| 1 (2 bytes) + |---Test SessionUid Size---| (4 bytes) + |---Test SessionUid Value---| (n bytes) + + |---Test ModulePath Id---| 1 (2 bytes) + |---Test ModulePath Size---| (4 bytes) + |---Test ModulePath Value---| (n bytes) + */ + + internal sealed class SuccessfulTestResultMessageSerializer : BaseSerializer, INamedPipeSerializer + { + public int Id => 5; + + public object Deserialize(Stream stream) + { + string? uid = null; + string? displayName = null; + string? state = null; + string? reason = null; + string? sessionUid = null; + string? modulePath = null; + + ushort fieldCount = ReadShort(stream); + + for (int i = 0; i < fieldCount; i++) + { + int fieldId = ReadShort(stream); + int fieldSize = ReadInt(stream); + + switch (fieldId) + { + case SuccessfulTestResultMessageFieldsId.Uid: + uid = ReadString(stream); + break; + + case SuccessfulTestResultMessageFieldsId.DisplayName: + displayName = ReadString(stream); + break; + + case SuccessfulTestResultMessageFieldsId.State: + state = ReadString(stream); + break; + + case SuccessfulTestResultMessageFieldsId.Reason: + reason = ReadString(stream); + break; + + case SuccessfulTestResultMessageFieldsId.SessionUid: + sessionUid = ReadString(stream); + break; + + case SuccessfulTestResultMessageFieldsId.ModulePath: + modulePath = ReadString(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 SuccessfulTestResultMessage(uid, displayName, state, reason, sessionUid, modulePath); + } + + public void Serialize(object objectToSerialize, Stream stream) + { + Debug.Assert(stream.CanSeek, "We expect a seekable stream."); + + var testResultMessage = (SuccessfulTestResultMessage)objectToSerialize; + + WriteShort(stream, GetFieldCount(testResultMessage)); + + WriteField(stream, SuccessfulTestResultMessageFieldsId.Uid, testResultMessage.Uid); + WriteField(stream, SuccessfulTestResultMessageFieldsId.DisplayName, testResultMessage.DisplayName); + WriteField(stream, SuccessfulTestResultMessageFieldsId.State, testResultMessage.State); + WriteField(stream, SuccessfulTestResultMessageFieldsId.Reason, testResultMessage.Reason); + WriteField(stream, SuccessfulTestResultMessageFieldsId.SessionUid, testResultMessage.SessionUid); + WriteField(stream, SuccessfulTestResultMessageFieldsId.ModulePath, testResultMessage.ModulePath); + } + + private static ushort GetFieldCount(SuccessfulTestResultMessage testResultMessage) => + (ushort)((testResultMessage.Uid is null ? 0 : 1) + + (testResultMessage.DisplayName is null ? 0 : 1) + + (testResultMessage.State is null ? 0 : 1) + + (testResultMessage.Reason is null ? 0 : 1) + + (testResultMessage.SessionUid is null ? 0 : 1) + + (testResultMessage.ModulePath is null ? 0 : 1)); + } + + /* + |---FieldCount---| 2 bytes + + |---Test Uid Id---| 1 (2 bytes) + |---Test Uid Size---| (4 bytes) + |---Test Uid Value---| (n bytes) + + |---Test DisplayName Id---| 1 (2 bytes) + |---Test DisplayName Size---| (4 bytes) + |---Test DisplayName Value---| (n bytes) + + |---Test State Id---| 1 (2 bytes) + |---Test State Size---| (4 bytes) + |---Test State Value---| (n bytes) + + |---Test Reason Id---| 1 (2 bytes) + |---Test Reason Size---| (4 bytes) + |---Test Reason Value---| (n bytes) + + |---Test ErrorMessage Id---| 1 (2 bytes) + |---Test ErrorMessage Size---| (4 bytes) + |---Test ErrorMessage Value---| (n bytes) + + |---Test ErrorStackTrace Id---| 1 (2 bytes) + |---Test ErrorStackTrace Size---| (4 bytes) + |---Test ErrorStackTrace Value---| (n bytes) + + |---Test SessionUid Id---| 1 (2 bytes) + |---Test SessionUid Size---| (4 bytes) + |---Test SessionUid Value---| (n bytes) + + |---Test ModulePath Id---| 1 (2 bytes) + |---Test ModulePath Size---| (4 bytes) + |---Test ModulePath Value---| (n bytes) + */ + + internal sealed class FailedTestResultMessageSerializer : BaseSerializer, INamedPipeSerializer + { + public int Id => 6; + + public object Deserialize(Stream stream) + { + string? uid = null; + string? displayName = null; + string? state = null; + string? reason = null; + string? errorMessage = null; + string? errorStackTrace = null; + string? sessionUid = null; + string? modulePath = null; + + ushort fieldCount = ReadShort(stream); + + for (int i = 0; i < fieldCount; i++) + { + int fieldId = ReadShort(stream); + int fieldSize = ReadInt(stream); + + switch (fieldId) + { + case FailedTestResultMessageFieldsId.Uid: + uid = ReadString(stream); + break; + + case FailedTestResultMessageFieldsId.DisplayName: + displayName = ReadString(stream); + break; + + case FailedTestResultMessageFieldsId.State: + state = ReadString(stream); + break; + + case FailedTestResultMessageFieldsId.Reason: + reason = ReadString(stream); + break; + + case FailedTestResultMessageFieldsId.ErrorMessage: + errorMessage = ReadString(stream); + break; + + case FailedTestResultMessageFieldsId.ErrorStackTrace: + errorStackTrace = ReadString(stream); + break; + + case FailedTestResultMessageFieldsId.SessionUid: + sessionUid = ReadString(stream); + break; + + case FailedTestResultMessageFieldsId.ModulePath: + modulePath = ReadString(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 FailedTestResultMessage(uid, displayName, state, reason, errorMessage, errorStackTrace, sessionUid, modulePath); + } + + public void Serialize(object objectToSerialize, Stream stream) + { + Debug.Assert(stream.CanSeek, "We expect a seekable stream."); + + var testResultMessage = (FailedTestResultMessage)objectToSerialize; + + WriteShort(stream, GetFieldCount(testResultMessage)); + + WriteField(stream, FailedTestResultMessageFieldsId.Uid, testResultMessage.Uid); + WriteField(stream, FailedTestResultMessageFieldsId.DisplayName, testResultMessage.DisplayName); + WriteField(stream, FailedTestResultMessageFieldsId.State, testResultMessage.State); + WriteField(stream, FailedTestResultMessageFieldsId.Reason, testResultMessage.Reason); + WriteField(stream, FailedTestResultMessageFieldsId.ErrorMessage, testResultMessage.ErrorMessage); + WriteField(stream, FailedTestResultMessageFieldsId.ErrorStackTrace, testResultMessage.ErrorStackTrace); + WriteField(stream, FailedTestResultMessageFieldsId.SessionUid, testResultMessage.SessionUid); + WriteField(stream, FailedTestResultMessageFieldsId.ModulePath, testResultMessage.ModulePath); + } + + private static ushort GetFieldCount(FailedTestResultMessage testResultMessage) => + (ushort)((testResultMessage.Uid is null ? 0 : 1) + + (testResultMessage.DisplayName is null ? 0 : 1) + + (testResultMessage.State is null ? 0 : 1) + + (testResultMessage.Reason is null ? 0 : 1) + + (testResultMessage.ErrorMessage is null ? 0 : 1) + + (testResultMessage.ErrorStackTrace is null ? 0 : 1) + + (testResultMessage.SessionUid is null ? 0 : 1) + + (testResultMessage.ModulePath is null ? 0 : 1)); + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestSessionEventSerializer.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestSessionEventSerializer.cs new file mode 100644 index 000000000000..67967a23ff33 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestSessionEventSerializer.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System.Diagnostics; + +namespace Microsoft.DotNet.Tools.Test +{ + /* + |---FieldCount---| 2 bytes + + |---Type Id---| 1 (2 bytes) + |---Type Size---| (4 bytes) + |---Type Value---| (n bytes) + + |---SessionUid Id---| 1 (2 bytes) + |---SessionUid Size---| (4 bytes) + |---SessionUid Value---| (n bytes) + + |---ModulePath Id---| 1 (2 bytes) + |---ModulePath Size---| (4 bytes) + |---ModulePath Value---| (n bytes) + */ + + internal sealed class TestSessionEventSerializer : BaseSerializer, INamedPipeSerializer + { + public int Id => 8; + + public object Deserialize(Stream stream) + { + string? type = null; + string? sessionUid = null; + string? modulePath = null; + + ushort fieldCount = ReadShort(stream); + + for (int i = 0; i < fieldCount; i++) + { + int fieldId = ReadShort(stream); + int fieldSize = ReadInt(stream); + + switch (fieldId) + { + case TestSessionEventFieldsId.SessionType: + type = ReadString(stream); + break; + + case TestSessionEventFieldsId.SessionUid: + sessionUid = ReadString(stream); + break; + + case TestSessionEventFieldsId.ModulePath: + modulePath = ReadString(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 TestSessionEvent(type, sessionUid, modulePath); + } + + public void Serialize(object objectToSerialize, Stream stream) + { + Debug.Assert(stream.CanSeek, "We expect a seekable stream."); + + var testSessionEvent = (TestSessionEvent)objectToSerialize; + + WriteShort(stream, GetFieldCount(testSessionEvent)); + + WriteField(stream, TestSessionEventFieldsId.SessionType, testSessionEvent.SessionType); + WriteField(stream, TestSessionEventFieldsId.SessionUid, testSessionEvent.SessionUid); + WriteField(stream, TestSessionEventFieldsId.ModulePath, testSessionEvent.ModulePath); + } + + private static ushort GetFieldCount(TestSessionEvent testSessionEvent) => + (ushort)((testSessionEvent.SessionType is null ? 0 : 1) + + (testSessionEvent.SessionUid is null ? 0 : 1) + + (testSessionEvent.ModulePath is null ? 0 : 1)); + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs b/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs index b8bac8397d78..c2ee6fc72b3f 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs @@ -11,9 +11,17 @@ internal class TestApplication private readonly string _modulePath; private readonly string _pipeName; private readonly string[] _args; + private readonly List _outputData = []; + private readonly List _errorData = []; + public event EventHandler HandshakeInfoReceived; public event EventHandler HelpRequested; + public event EventHandler SuccessfulTestResultReceived; + public event EventHandler FailedTestResultReceived; + public event EventHandler FileArtifactInfoReceived; + public event EventHandler SessionEventReceived; public event EventHandler ErrorReceived; + public event EventHandler TestProcessExited; public string ModulePath => _modulePath; @@ -24,11 +32,11 @@ public TestApplication(string modulePath, string pipeName, string[] args) _args = args; } - public async Task RunAsync() + public async Task RunAsync(bool enableHelp) { if (!ModulePathExists()) { - return; + return 1; } bool isDll = _modulePath.EndsWith(".dll"); @@ -37,39 +45,50 @@ public async Task RunAsync() FileName = isDll ? Environment.ProcessPath : _modulePath, - Arguments = BuildArgs(isDll) + Arguments = enableHelp ? BuildHelpArgs(isDll) : BuildArgs(isDll), + RedirectStandardOutput = true, + RedirectStandardError = true }; + return await StartProcess(processStartInfo); + } + + private async Task StartProcess(ProcessStartInfo processStartInfo) + { if (VSTestTrace.TraceEnabled) { VSTestTrace.SafeWriteTrace(() => $"Updated args: {processStartInfo.Arguments}"); } - await Process.Start(processStartInfo).WaitForExitAsync(); + var process = Process.Start(processStartInfo); + StoreOutputAndErrorData(process); + await process.WaitForExitAsync(); + + TestProcessExited?.Invoke(this, new TestProcessExitEventArgs { OutputData = _outputData, ErrorData = _errorData, ExitCode = process.ExitCode }); + + return process.ExitCode; } - public async Task RunHelpAsync() + private void StoreOutputAndErrorData(Process process) { - if (!ModulePathExists()) - { - return; - } + process.EnableRaisingEvents = true; - bool isDll = _modulePath.EndsWith(".dll"); - ProcessStartInfo processStartInfo = new() + process.OutputDataReceived += (sender, e) => { - FileName = isDll ? - Environment.ProcessPath : - _modulePath, - Arguments = BuildHelpArgs(isDll) - }; + if (string.IsNullOrEmpty(e.Data)) + return; - if (VSTestTrace.TraceEnabled) + _outputData.Add(e.Data); + }; + process.ErrorDataReceived += (sender, e) => { - VSTestTrace.SafeWriteTrace(() => $"Updated args: {processStartInfo.Arguments}"); - } + if (string.IsNullOrEmpty(e.Data)) + return; - await Process.Start(processStartInfo).WaitForExitAsync(); + _errorData.Add(e.Data); + }; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); } private bool ModulePathExists() @@ -114,9 +133,34 @@ private string BuildHelpArgs(bool isDll) return builder.ToString(); } + public void OnHandshakeInfo(HandshakeInfo handshakeInfo) + { + HandshakeInfoReceived?.Invoke(this, new HandshakeInfoArgs { handshakeInfo = handshakeInfo }); + } + public void OnCommandLineOptionMessages(CommandLineOptionMessages commandLineOptionMessages) { HelpRequested?.Invoke(this, new HelpEventArgs { CommandLineOptionMessages = commandLineOptionMessages }); } + + internal void OnSuccessfulTestResultMessage(SuccessfulTestResultMessage successfulTestResultMessage) + { + SuccessfulTestResultReceived?.Invoke(this, new SuccessfulTestResultEventArgs { SuccessfulTestResultMessage = successfulTestResultMessage }); + } + + internal void OnFailedTestResultMessage(FailedTestResultMessage failedTestResultMessage) + { + FailedTestResultReceived?.Invoke(this, new FailedTestResultEventArgs { FailedTestResultMessage = failedTestResultMessage }); + } + + internal void OnFileArtifactInfo(FileArtifactInfo fileArtifactInfo) + { + FileArtifactInfoReceived?.Invoke(this, new FileArtifactInfoEventArgs { FileArtifactInfo = fileArtifactInfo }); + } + + internal void OnSessionEvent(TestSessionEvent sessionEvent) + { + SessionEventReceived?.Invoke(this, new SessionEventArgs { SessionEvent = sessionEvent }); + } } } diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.Help.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.Help.cs index c163f79fc286..faf63f1e3699 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.Help.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.Help.cs @@ -45,9 +45,9 @@ private void OnHelpRequested(object sender, HelpEventArgs args) foreach (CommandLineOptionMessage commandLineOptionMessage in commandLineOptionMessages.CommandLineOptionMessageList) { - if (commandLineOptionMessage.IsHidden) continue; + if (commandLineOptionMessage.IsHidden.HasValue && commandLineOptionMessage.IsHidden.Value) continue; - if (commandLineOptionMessage.IsBuiltIn) + if (commandLineOptionMessage.IsBuiltIn.HasValue && commandLineOptionMessage.IsBuiltIn.Value) { builtInOptions.Add(commandLineOptionMessage.Name); } @@ -77,9 +77,9 @@ private Dictionary> GetAllOptions() foreach (KeyValuePair option in _commandLineOptionNameToModuleNames) { - if (!builtInToOptions.TryGetValue(option.Value.IsBuiltIn, out List value)) + if (!builtInToOptions.TryGetValue(option.Value.IsBuiltIn.Value, out List value)) { - builtInToOptions.Add(option.Value.IsBuiltIn, [option.Value]); + builtInToOptions.Add(option.Value.IsBuiltIn.Value, [option.Value]); } else { diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 866ef0a9dec1..e044c8238db8 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -46,17 +46,24 @@ public int Run(ParseResult parseResult) { testApp.HelpRequested += OnHelpRequested; testApp.ErrorReceived += OnErrorReceived; + testApp.TestProcessExited += OnTestProcessExited; - await testApp.RunHelpAsync(); + int runHelpResult = await testApp.RunAsync(enableHelp: true); }); } else { _actionQueue = new(degreeOfParallelism, async (TestApplication testApp) => { + testApp.HandshakeInfoReceived += OnHandshakeInfoReceived; + testApp.SuccessfulTestResultReceived += OnTestResultReceived; + testApp.FailedTestResultReceived += OnTestResultReceived; + testApp.FileArtifactInfoReceived += OnFileArtifactInfoReceived; + testApp.SessionEventReceived += OnSessionEventReceived; testApp.ErrorReceived += OnErrorReceived; + testApp.TestProcessExited += OnTestProcessExited; - await testApp.RunAsync(); + int runResult = await testApp.RunAsync(enableHelp: false); }); } @@ -70,7 +77,10 @@ public int Run(ParseResult parseResult) AddAdditionalMSBuildParameters(parseResult, msbuildCommandlineArgs); - VSTestTrace.SafeWriteTrace(() => $"MSBuild command line arguments: {msbuildCommandlineArgs}"); + if (VSTestTrace.TraceEnabled) + { + VSTestTrace.SafeWriteTrace(() => $"MSBuild command line arguments: {string.Join(" ", msbuildCommandlineArgs)}"); + } ForwardingAppImplementation msBuildForwardingApp = new(GetMSBuildExePath(), msbuildCommandlineArgs); int testsProjectResult = msBuildForwardingApp.Execute(); @@ -131,36 +141,69 @@ private Task OnRequest(IRequest request) { try { - if (TryGetModulePath(request, out string modulePath)) - { - _testApplications[modulePath] = new TestApplication(modulePath, _pipeNameDescription.Name, _args); - // Write the test application to the channel - _actionQueue.Enqueue(_testApplications[modulePath]); - - return Task.FromResult((IResponse)VoidResponse.CachedInstance); - } - - if (TryGetHelpResponse(request, out CommandLineOptionMessages commandLineOptionMessages)) - { - var testApplication = _testApplications[commandLineOptionMessages.ModulePath]; - Debug.Assert(testApplication is not null); - testApplication.OnCommandLineOptionMessages(commandLineOptionMessages); - - return Task.FromResult((IResponse)VoidResponse.CachedInstance); - } - - // If we don't recognize the message, log and skip it - if (TryGetUnknownMessage(request, out UnknownMessage unknownMessage)) + switch (request) { - if (VSTestTrace.TraceEnabled) - { - VSTestTrace.SafeWriteTrace(() => $"Request '{request.GetType()}' with Serializer ID = {unknownMessage.SerializerId} is unsupported."); - } - return Task.FromResult((IResponse)VoidResponse.CachedInstance); + case Module module: + string modulePath = module.DLLPath; + _testApplications[modulePath] = new TestApplication(modulePath, _pipeNameDescription.Name, _args); + // Write the test application to the channel + _actionQueue.Enqueue(_testApplications[modulePath]); + break; + + case HandshakeInfo handshakeInfo: + if (handshakeInfo.Properties.TryGetValue(HandshakeInfoPropertyNames.ModulePath, out string value)) + { + var testApp = _testApplications[value]; + Debug.Assert(testApp is not null); + testApp.OnHandshakeInfo(handshakeInfo); + + return Task.FromResult((IResponse)CreateHandshakeInfo()); + } + break; + + case CommandLineOptionMessages commandLineOptionMessages: + var testApplication = _testApplications[commandLineOptionMessages.ModulePath]; + Debug.Assert(testApplication is not null); + testApplication.OnCommandLineOptionMessages(commandLineOptionMessages); + break; + + case SuccessfulTestResultMessage successfulTestResultMessage: + testApplication = _testApplications[successfulTestResultMessage.ModulePath]; + Debug.Assert(testApplication is not null); + + testApplication.OnSuccessfulTestResultMessage(successfulTestResultMessage); + break; + + case FailedTestResultMessage failedTestResultMessage: + testApplication = _testApplications[failedTestResultMessage.ModulePath]; + Debug.Assert(testApplication is not null); + + testApplication.OnFailedTestResultMessage(failedTestResultMessage); + break; + + case FileArtifactInfo fileArtifactInfo: + testApplication = _testApplications[fileArtifactInfo.ModulePath]; + Debug.Assert(testApplication is not null); + testApplication.OnFileArtifactInfo(fileArtifactInfo); + break; + + case TestSessionEvent sessionEvent: + testApplication = _testApplications[sessionEvent.ModulePath]; + Debug.Assert(testApplication is not null); + testApplication.OnSessionEvent(sessionEvent); + break; + + // If we don't recognize the message, log and skip it + case UnknownMessage unknownMessage: + if (VSTestTrace.TraceEnabled) + { + VSTestTrace.SafeWriteTrace(() => $"Request '{request.GetType()}' with Serializer ID = {unknownMessage.SerializerId} is unsupported."); + } + return Task.FromResult((IResponse)VoidResponse.CachedInstance); + default: + // If it doesn't match any of the above, throw an exception + throw new NotSupportedException($"Request '{request.GetType()}' is unsupported."); } - - // If it doesn't match any of the above, throw an exception - throw new NotSupportedException($"Request '{request.GetType()}' is unsupported."); } catch (Exception ex) { @@ -171,50 +214,110 @@ private Task OnRequest(IRequest request) Environment.FailFast(ex.ToString()); } + return Task.FromResult((IResponse)VoidResponse.CachedInstance); } - private static bool TryGetModulePath(IRequest request, out string modulePath) + private static HandshakeInfo CreateHandshakeInfo() => + new(new Dictionary + { + { HandshakeInfoPropertyNames.PID, Process.GetCurrentProcess().Id.ToString() }, + { HandshakeInfoPropertyNames.Architecture, RuntimeInformation.OSArchitecture.ToString() }, + { HandshakeInfoPropertyNames.Framework, RuntimeInformation.FrameworkDescription }, + { HandshakeInfoPropertyNames.OS, RuntimeInformation.OSDescription }, + { HandshakeInfoPropertyNames.ProtocolVersion, ProtocolConstants.Version } + }); + + private void OnHandshakeInfoReceived(object sender, HandshakeInfoArgs args) + { + if (!VSTestTrace.TraceEnabled) + { + return; + } + + var handshakeInfo = args.handshakeInfo; + + foreach (var property in handshakeInfo.Properties) + { + VSTestTrace.SafeWriteTrace(() => $"{property.Key}: {property.Value}"); + } + } + private void OnTestResultReceived(object sender, EventArgs args) { - if (request is Module module) + if (!VSTestTrace.TraceEnabled) { - modulePath = module.DLLPath; - return true; + return; } - modulePath = null; - return false; + if (args is SuccessfulTestResultEventArgs successfulTestResultEventArgs) + { + var successfulTestResultMessage = successfulTestResultEventArgs.SuccessfulTestResultMessage; + VSTestTrace.SafeWriteTrace(() => $"TestResultMessage: {successfulTestResultMessage.Uid}, {successfulTestResultMessage.DisplayName}, " + + $"{successfulTestResultMessage.State}, {successfulTestResultMessage.Reason}, {successfulTestResultMessage.SessionUid}, {successfulTestResultMessage.ModulePath}"); + } + else if (args is FailedTestResultEventArgs failedTestResultEventArgs) + { + var failedTestResultMessage = failedTestResultEventArgs.FailedTestResultMessage; + VSTestTrace.SafeWriteTrace(() => $"TestResultMessage: {failedTestResultMessage.Uid}, {failedTestResultMessage.DisplayName}, " + + $"{failedTestResultMessage.State}, {failedTestResultMessage.Reason}, {failedTestResultMessage.ErrorMessage}," + + $" {failedTestResultMessage.ErrorStackTrace}, {failedTestResultMessage.SessionUid}, {failedTestResultMessage.ModulePath}"); + } } - private static bool TryGetHelpResponse(IRequest request, out CommandLineOptionMessages commandLineOptionMessages) + private void OnFileArtifactInfoReceived(object sender, FileArtifactInfoEventArgs args) { - if (request is CommandLineOptionMessages result) + if (!VSTestTrace.TraceEnabled) { - commandLineOptionMessages = result; - return true; + return; } - commandLineOptionMessages = null; - return false; + var fileArtifactInfo = args.FileArtifactInfo; + VSTestTrace.SafeWriteTrace(() => $"FileArtifactInfo: {fileArtifactInfo.FullPath}, {fileArtifactInfo.DisplayName}, " + + $"{fileArtifactInfo.Description}, {fileArtifactInfo.TestUid}, {fileArtifactInfo.TestDisplayName}, " + + $"{fileArtifactInfo.SessionUid}, {fileArtifactInfo.ModulePath}"); } - private static bool TryGetUnknownMessage(IRequest request, out UnknownMessage unknownMessage) + private void OnSessionEventReceived(object sender, SessionEventArgs args) { - if (request is UnknownMessage result) + if (!VSTestTrace.TraceEnabled) { - unknownMessage = result; - return true; + return; } - unknownMessage = null; - return false; + var sessionEvent = args.SessionEvent; + VSTestTrace.SafeWriteTrace(() => $"TestSessionEvent: {sessionEvent.SessionType}, {sessionEvent.SessionUid}, {sessionEvent.ModulePath}"); } private void OnErrorReceived(object sender, ErrorEventArgs args) { - if (VSTestTrace.TraceEnabled) + if (!VSTestTrace.TraceEnabled) + { + return; + } + + VSTestTrace.SafeWriteTrace(() => args.ErrorMessage); + } + + private void OnTestProcessExited(object sender, TestProcessExitEventArgs args) + { + if (!VSTestTrace.TraceEnabled) + { + return; + } + + if (args.ExitCode != 0) + { + VSTestTrace.SafeWriteTrace(() => $"Test Process exited with non-zero exit code: {args.ExitCode}"); + } + + if (args.OutputData.Count > 0) + { + VSTestTrace.SafeWriteTrace(() => $"Output Data: {string.Join("\n", args.OutputData)}"); + } + + if (args.ErrorData.Count > 0) { - VSTestTrace.SafeWriteTrace(() => args.ErrorMessage); + VSTestTrace.SafeWriteTrace(() => $"Error Data: {string.Join("\n", args.ErrorData)}"); } }