diff --git a/samples/Playground/Program.cs b/samples/Playground/Program.cs index 5454539f68..4350216b80 100644 --- a/samples/Playground/Program.cs +++ b/samples/Playground/Program.cs @@ -4,6 +4,9 @@ using System.Reflection; using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.TestHostControllers; +using Microsoft.Testing.Platform.Messages; using Microsoft.Testing.Platform.ServerMode.IntegrationTests.Messages.V100; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -25,6 +28,9 @@ public static async Task Main(string[] args) ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); testApplicationBuilder.AddMSTest(() => [Assembly.GetEntryAssembly()!]); + // Custom test host controller extension + // testApplicationBuilder.TestHostControllers.AddProcessLifetimeHandler(s => new OutOfProc(s.GetMessageBus())); + // Enable Trx // testApplicationBuilder.AddTrxReportProvider(); @@ -56,3 +62,38 @@ public static async Task Main(string[] args) } } } + +public class OutOfProc : ITestHostProcessLifetimeHandler, IDataProducer +{ + private readonly IMessageBus _messageBus; + + public string Uid + => nameof(OutOfProc); + + public string Version + => "1.0.0"; + + public string DisplayName + => nameof(OutOfProc); + + public string Description + => nameof(OutOfProc); + + public Type[] DataTypesProduced + => [typeof(FileArtifact)]; + + public Task BeforeTestHostProcessStartAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task IsEnabledAsync() + => Task.FromResult(true); + + public OutOfProc(IMessageBus messageBus) + => _messageBus = messageBus; + + public async Task OnTestHostProcessExitedAsync(ITestHostProcessInformation testHostProcessInformation, CancellationToken cancellation) + => await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(@"C:\sampleFile"), "Sample", "sample description")); + + public Task OnTestHostProcessStartedAsync(ITestHostProcessInformation testHostProcessInformation, CancellationToken cancellation) + => Task.CompletedTask; +} diff --git a/samples/Playground/Tests.cs b/samples/Playground/Tests.cs index b94a2cde5d..4ea420bd9c 100644 --- a/samples/Playground/Tests.cs +++ b/samples/Playground/Tests.cs @@ -15,9 +15,7 @@ public class TestClass public TestContext TestContext { get; set; } [TestMethod] - public void Test() - { - } + public void Test() => TestContext.AddResultFile(@"c:\hello2"); [TestMethod] public void Test2() diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Capabilities/VSTestBridgeExtensionBaseCapabilities.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Capabilities/VSTestBridgeExtensionBaseCapabilities.cs index bcf444bd2b..dade42a2bf 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Capabilities/VSTestBridgeExtensionBaseCapabilities.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Capabilities/VSTestBridgeExtensionBaseCapabilities.cs @@ -8,7 +8,6 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.Capabilities; public sealed class VSTestBridgeExtensionBaseCapabilities : ITrxReportCapability, IVSTestFlattenedTestNodesReportCapability, INamedFeatureCapability { - private const string MultiRequestSupport = "experimental_multiRequestSupport"; private const string VSTestProviderSupport = "vstestProvider"; /// @@ -25,5 +24,5 @@ public sealed class VSTestBridgeExtensionBaseCapabilities : ITrxReportCapability /// void ITrxReportCapability.Enable() => IsTrxEnabled = true; - bool INamedFeatureCapability.IsSupported(string featureName) => featureName is MultiRequestSupport or VSTestProviderSupport; + bool INamedFeatureCapability.IsSupported(string featureName) => featureName is VSTestProviderSupport; } diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs index 4747874a88..2818df83d2 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs @@ -22,7 +22,7 @@ namespace Microsoft.Testing.Platform.Hosts; internal sealed partial class ServerTestHost : CommonTestHost, IServerTestHost, IDisposable { - private const string ProtocolVersion = "1.0.0"; + public const string ProtocolVersion = "1.0.0"; private readonly Func> _buildTestFrameworkAsync; private readonly IMessageHandlerFactory _messageHandlerFactory; @@ -389,8 +389,11 @@ private async Task HandleRequestCoreAsync(RequestMessage message, RpcInv Capabilities: new ServerCapabilities( new ServerTestingCapabilities( SupportsDiscovery: true, - MultiRequestSupport: namedFeatureCapability?.IsSupported(JsonRpcStrings.MultiRequestSupport) == true, - VSTestProviderSupport: namedFeatureCapability?.IsSupported(JsonRpcStrings.VSTestProviderSupport) == true))); + // Current implementation of testing platform and VS doesn't allow multi-request. + MultiRequestSupport: false, + VSTestProviderSupport: namedFeatureCapability?.IsSupported(JsonRpcStrings.VSTestProviderSupport) == true, + SupportsAttachments: true, + MultiConnectionProvider: false))); } case (JsonRpcMethods.TestingDiscoverTests, DiscoverRequestArgs args): @@ -578,7 +581,7 @@ private async Task SendErrorAsync(int reqId, int errorCode, string message, obje using (await _messageMonitor.LockAsync(cancellationToken)) { - await _messageHandler!.WriteRequestAsync(error, cancellationToken); + await _messageHandler.WriteRequestAsync(error, cancellationToken); } } @@ -589,7 +592,7 @@ private async Task SendResponseAsync(int reqId, object result, CancellationToken using (await _messageMonitor.LockAsync(cancellationToken)) { - await _messageHandler!.WriteRequestAsync(response, cancellationToken); + await _messageHandler.WriteRequestAsync(response, cancellationToken); } } @@ -630,8 +633,8 @@ public void Dispose() // We could consider creating a stateful engine that has the lifetime == server connection UP. } - internal Task SendTestUpdateCompleteAsync(Guid runId) - => SendTestUpdateAsync(new TestNodeStateChangedEventArgs(runId, Changes: null)); + internal async Task SendTestUpdateCompleteAsync(Guid runId) + => await SendTestUpdateAsync(new TestNodeStateChangedEventArgs(runId, Changes: null)); public async Task SendTestUpdateAsync(TestNodeStateChangedEventArgs update) => await SendMessageAsync( diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs index c5fd017160..13d7f13ad9 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs @@ -392,6 +392,19 @@ await LogTestHostCreatedAsync( systemEnvironment.GetEnvironmentVariable($"{EnvironmentVariableConstants.TESTINGPLATFORM_TESTHOSTCONTROLLER_SKIPEXTENSION}_{testHostControllerInfo.GetTestHostControllerPID(true)}") != "1") && !commandLineHandler.IsOptionSet(PlatformCommandLineProvider.DiscoverTestsOptionKey)) { + PassiveNode? passiveNode = null; + if (hasServerFlag && isJsonRpcProtocol) + { + // Build the IMessageHandlerFactory for the PassiveNode + IMessageHandlerFactory messageHandlerFactory = ((ServerModeManager)ServerMode).Build(serviceProvider); + passiveNode = new PassiveNode( + messageHandlerFactory, + testApplicationCancellationTokenSource, + processHandler, + systemMonitorAsyncFactory, + loggerFactory.CreateLogger()); + } + // Clone the service provider to avoid to add the message bus proxy to the main service provider. var testHostControllersServiceProvider = (ServiceProvider)serviceProvider.Clone(); @@ -404,7 +417,7 @@ await LogTestHostCreatedAsync( if (testHostControllers.RequireProcessRestart) { testHostControllerInfo.IsCurrentProcessTestHostController = true; - TestHostControllersTestHost testHostControllersTestHost = new(testHostControllers, testHostControllersServiceProvider, systemEnvironment, loggerFactory, systemClock); + TestHostControllersTestHost testHostControllersTestHost = new(testHostControllers, testHostControllersServiceProvider, passiveNode, systemEnvironment, loggerFactory, systemClock); await LogTestHostCreatedAsync( serviceProvider, diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs index 25f110f315..f88b94fa54 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs @@ -29,6 +29,7 @@ namespace Microsoft.Testing.Platform.Hosts; internal sealed class TestHostControllersTestHost : CommonTestHost, ITestHost, IDisposable, IOutputDeviceDataProducer { private readonly TestHostControllerConfiguration _testHostsInformation; + private readonly PassiveNode? _passiveNode; private readonly IEnvironment _environment; private readonly IClock _clock; private readonly ILoggerFactory _loggerFactory; @@ -39,11 +40,12 @@ internal sealed class TestHostControllersTestHost : CommonTestHost, ITestHost, I private int? _testHostExitCode; private int? _testHostPID; - public TestHostControllersTestHost(TestHostControllerConfiguration testHostsInformation, ServiceProvider serviceProvider, IEnvironment environment, + public TestHostControllersTestHost(TestHostControllerConfiguration testHostsInformation, ServiceProvider serviceProvider, PassiveNode? passiveNode, IEnvironment environment, ILoggerFactory loggerFactory, IClock clock) : base(serviceProvider) { _testHostsInformation = testHostsInformation; + _passiveNode = passiveNode; _environment = environment; _clock = clock; _loggerFactory = loggerFactory; @@ -124,21 +126,34 @@ protected override async Task InternalRunAsync() List dataConsumersBuilder = [.. _testHostsInformation.DataConsumer]; + // We add the IPlatformOutputDevice after all users extensions. IPlatformOutputDevice? display = ServiceProvider.GetServiceInternal(); if (display is IDataConsumer dataConsumerDisplay) { dataConsumersBuilder.Add(dataConsumerDisplay); } - IPushOnlyProtocol? pushOnlyProtocol = ServiceProvider.GetService(); // We register the DotnetTestDataConsumer as last to ensure that it will be the last one to consume the data. + IPushOnlyProtocol? pushOnlyProtocol = ServiceProvider.GetService(); if (pushOnlyProtocol?.IsServerMode == true) { - RoslynDebug.Assert(pushOnlyProtocol is not null); - dataConsumersBuilder.Add(await pushOnlyProtocol.GetDataConsumerAsync()); } + // If we're in server mode jsonrpc we add as last consumer the PassiveNodeDataConsumer for the attachments. + // Connect the passive node if it's available + if (_passiveNode is not null) + { + if (await _passiveNode.ConnectAsync()) + { + dataConsumersBuilder.Add(new PassiveNodeDataConsumer(_passiveNode)); + } + else + { + await _logger.LogWarningAsync("PassiveNode was expected to connect but failed"); + } + } + AsynchronousMessageBus concreteMessageBusService = new( dataConsumersBuilder.ToArray(), ServiceProvider.GetTestApplicationCancellationTokenSource(), 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 a3aa049f3d..c3410c2152 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Json.cs @@ -88,6 +88,8 @@ public Json(Dictionary? serializers = null, Dictionary(artifact => @@ -334,6 +336,22 @@ public Json(Dictionary? serializers = null, Dictionary(info => + new (string, object?)[] + { + (JsonRpcStrings.Attachments, info.Attachments), + }); + + _serializers[typeof(RunTestAttachment)] = new JsonObjectSerializer(info => + new (string, object?)[] + { + (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)); @@ -474,7 +492,9 @@ public Json(Dictionary? serializers = null, Dictionary new ServerTestingCapabilities( SupportsDiscovery: json.Bind(jsonElement, JsonRpcStrings.SupportsDiscovery), MultiRequestSupport: json.Bind(jsonElement, JsonRpcStrings.MultiRequestSupport), - VSTestProviderSupport: json.Bind(jsonElement, JsonRpcStrings.VSTestProviderSupport))); + 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) => { diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/JsonRpcMethods.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/JsonRpcMethods.cs index e0a988755e..6aa3790892 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/JsonRpcMethods.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/JsonRpcMethods.cs @@ -16,6 +16,7 @@ internal static class JsonRpcMethods public const string ClientLog = "client/log"; public const string Exit = "exit"; public const string CancelRequest = "$/cancelRequest"; + public const string TestingTestUpdatesAttachments = "testing/testUpdates/attachments"; } internal static class JsonRpcStrings @@ -44,6 +45,8 @@ internal static class JsonRpcStrings public const string SupportsDiscovery = "supportsDiscovery"; public const string MultiRequestSupport = "experimental_multiRequestSupport"; public const string VSTestProviderSupport = "vstestProvider"; + public const string AttachmentsSupport = "attachmentsSupport"; + public const string MultiConnectionProvider = "multipleConnectionProvider"; // Discovery and run public const string RunId = "runId"; diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/PassiveNode.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/PassiveNode.cs new file mode 100644 index 0000000000..52c8b93d64 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/PassiveNode.cs @@ -0,0 +1,115 @@ +// 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.CodeAnalysis; + +using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.Hosts; +using Microsoft.Testing.Platform.Logging; +using Microsoft.Testing.Platform.Services; + +namespace Microsoft.Testing.Platform.ServerMode; + +internal class PassiveNode : IDisposable +{ + private readonly IMessageHandlerFactory _messageHandlerFactory; + private readonly ITestApplicationCancellationTokenSource _testApplicationCancellationTokenSource; + private readonly IProcessHandler _processHandler; + private readonly ILogger _logger; + private readonly IAsyncMonitor _messageMonitor; + private IMessageHandler? _messageHandler; + + public PassiveNode( + IMessageHandlerFactory messageHandlerFactory, + ITestApplicationCancellationTokenSource testApplicationCancellationTokenSource, + IProcessHandler processHandler, + IAsyncMonitorFactory asyncMonitorFactory, + ILogger logger) + { + _messageHandlerFactory = messageHandlerFactory; + _testApplicationCancellationTokenSource = testApplicationCancellationTokenSource; + _processHandler = processHandler; + _messageMonitor = asyncMonitorFactory.Create(); + _logger = logger; + } + + [MemberNotNull(nameof(_messageHandler))] + public void AssertInitialized() + { + if (_messageHandler is null) + { + throw new InvalidOperationException(); + } + } + + public async Task ConnectAsync() + { + // Create message handler + await _logger.LogDebugAsync("Create message handler"); + _messageHandler = await _messageHandlerFactory.CreateMessageHandlerAsync(_testApplicationCancellationTokenSource.CancellationToken); + + // Wait the initial message + await _logger.LogDebugAsync("Wait the initial message"); + RpcMessage? message = await _messageHandler.ReadAsync(_testApplicationCancellationTokenSource.CancellationToken); + if (message is null) + { + return false; + } + + // Log the message + if (_logger.IsEnabled(LogLevel.Trace)) + { + await _logger.LogTraceAsync(message!.ToString()); + } + + var requestMessage = (RequestMessage)message; + var responseObject = new InitializeResponseArgs( + ProcessId: _processHandler.GetCurrentProcess().Id, + ServerInfo: new ServerInfo("test-anywhere", Version: ServerTestHost.ProtocolVersion), + Capabilities: new ServerCapabilities( + new ServerTestingCapabilities( + SupportsDiscovery: false, + MultiRequestSupport: false, + VSTestProviderSupport: false, + // This means we push attachments + SupportsAttachments: true, + // This means we're a push node + MultiConnectionProvider: true))); + + await SendResponseAsync(requestMessage.Id, responseObject, _testApplicationCancellationTokenSource.CancellationToken); + return true; + } + + private async Task SendResponseAsync(int reqId, object result, CancellationToken cancellationToken) + { + AssertInitialized(); + + ResponseMessage response = new(reqId, result); + using (await _messageMonitor.LockAsync(cancellationToken)) + { + await _messageHandler.WriteRequestAsync(response, cancellationToken); + } + } + + public async Task SendAttachmentsAsync(TestsAttachments testsAttachments, CancellationToken cancellationToken) + { + AssertInitialized(); + + NotificationMessage notification = new(JsonRpcMethods.TestingTestUpdatesAttachments, testsAttachments); + using (await _messageMonitor.LockAsync(cancellationToken)) + { + await _messageHandler.WriteRequestAsync(notification, cancellationToken); + } + } + + public void Dispose() + { + if (_messageHandler != null) + { + if (_messageHandler is IDisposable disposable) + { + disposable.Dispose(); + } + } + } +} diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/PerRequestServerDataConsumerService.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/PerRequestServerDataConsumerService.cs index 15ffd7070c..e728b45fcf 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/PerRequestServerDataConsumerService.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/PerRequestServerDataConsumerService.cs @@ -21,6 +21,7 @@ namespace Microsoft.Testing.Platform.ServerMode; internal sealed class PerRequestServerDataConsumer(IServiceProvider serviceProvider, IServerTestHost serverTestHost, Guid runId, ITask task) : IDataConsumer, ITestSessionLifetimeHandler, IDisposable { private const int TestNodeUpdateDelayInMs = 200; + private const string FileType = "file"; private readonly ConcurrentDictionary _testNodeUidToStateStatistics = new(); private readonly ConcurrentDictionary _discoveredTestNodeUids = new(); @@ -224,15 +225,15 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella } else if (value is SessionFileArtifact sessionFileArtifact) { - Artifacts.Add(new Artifact(sessionFileArtifact.FileInfo.FullName, dataProducer.Uid, "file", sessionFileArtifact.DisplayName, sessionFileArtifact.Description)); + Artifacts.Add(new Artifact(sessionFileArtifact.FileInfo.FullName, dataProducer.Uid, FileType, sessionFileArtifact.DisplayName, sessionFileArtifact.Description)); } else if (value is FileArtifact file) { - Artifacts.Add(new Artifact(file.FileInfo.FullName, dataProducer.Uid, "file", file.DisplayName, file.Description)); + Artifacts.Add(new Artifact(file.FileInfo.FullName, dataProducer.Uid, FileType, file.DisplayName, file.Description)); } else if (value is TestNodeFileArtifact testNodeFileArtifact) { - Artifacts.Add(new Artifact(testNodeFileArtifact.FileInfo.FullName, dataProducer.Uid, "file", testNodeFileArtifact.DisplayName, testNodeFileArtifact.Description)); + Artifacts.Add(new Artifact(testNodeFileArtifact.FileInfo.FullName, dataProducer.Uid, FileType, testNodeFileArtifact.DisplayName, testNodeFileArtifact.Description)); } } diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/RpcMessages.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/RpcMessages.cs index 621c3d2257..be95ad9a19 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/RpcMessages.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/RpcMessages.cs @@ -72,7 +72,9 @@ internal record ServerCapabilities(ServerTestingCapabilities TestingCapabilities internal record ServerTestingCapabilities( bool SupportsDiscovery, bool MultiRequestSupport, - bool VSTestProviderSupport); + bool VSTestProviderSupport, + bool SupportsAttachments, + bool MultiConnectionProvider); internal record TestNodeStateChangedEventArgs(Guid RunId, TestNodeUpdateMessage[]? Changes); @@ -83,3 +85,7 @@ internal record TelemetryEventArgs(string EventName, IDictionary internal record ProcessInfoArgs(string Program, string? Args, string? WorkingDirectory, IDictionary? EnvironmentVariables); internal record AttachDebuggerInfoArgs(int ProcessId); + +internal record class TestsAttachments(RunTestAttachment[] Attachments); + +internal record class RunTestAttachment(string? Uri, string? Producer, string? Type, string? DisplayName, string? Description); diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/SerializerUtilities.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/SerializerUtilities.cs index c39239cb48..045d23d934 100644 --- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/SerializerUtilities.cs +++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/SerializerUtilities.cs @@ -125,6 +125,8 @@ static SerializerUtilities() [JsonRpcStrings.SupportsDiscovery] = capabilities.SupportsDiscovery, [JsonRpcStrings.MultiRequestSupport] = capabilities.MultiRequestSupport, [JsonRpcStrings.VSTestProviderSupport] = capabilities.VSTestProviderSupport, + [JsonRpcStrings.AttachmentsSupport] = capabilities.SupportsAttachments, + [JsonRpcStrings.MultiConnectionProvider] = capabilities.MultiConnectionProvider, }); Serializers[typeof(Artifact)] = new ObjectSerializer(res => new Dictionary @@ -444,6 +446,30 @@ static SerializerUtilities() return values; }); + Serializers[typeof(TestsAttachments)] = new ObjectSerializer(ev => + { + Dictionary values = new() + { + [JsonRpcStrings.Attachments] = ev.Attachments.Select(x => Serialize(x)).ToArray(), + }; + + return values; + }); + + Serializers[typeof(RunTestAttachment)] = new ObjectSerializer(ev => + { + Dictionary values = new() + { + [JsonRpcStrings.Uri] = ev.Uri, + [JsonRpcStrings.Producer] = ev.Producer, + [JsonRpcStrings.Type] = ev.Type, + [JsonRpcStrings.DisplayName] = ev.DisplayName, + [JsonRpcStrings.Description] = ev.Description, + }; + + return values; + }); + // Deserialize a generic JSON-RPC message Deserializers[typeof(RpcMessage)] = new ObjectDeserializer(properties => { @@ -555,11 +581,15 @@ static SerializerUtilities() bool supportsDiscovery = GetRequiredPropertyFromJson(testingCapabilities, JsonRpcStrings.SupportsDiscovery); bool multiRequestSupport = GetRequiredPropertyFromJson(testingCapabilities, JsonRpcStrings.MultiRequestSupport); bool vstestProviderSupport = GetRequiredPropertyFromJson(testingCapabilities, JsonRpcStrings.VSTestProviderSupport); + bool attachmentsSupport = GetRequiredPropertyFromJson(testingCapabilities, JsonRpcStrings.AttachmentsSupport); + bool multiConnectionProvider = GetRequiredPropertyFromJson(testingCapabilities, JsonRpcStrings.MultiConnectionProvider); return new ServerCapabilities(new ServerTestingCapabilities( SupportsDiscovery: supportsDiscovery, MultiRequestSupport: multiRequestSupport, - VSTestProviderSupport: vstestProviderSupport)); + VSTestProviderSupport: vstestProviderSupport, + SupportsAttachments: attachmentsSupport, + MultiConnectionProvider: multiConnectionProvider)); }); Deserializers[typeof(DiscoverRequestArgs)] = new ObjectDeserializer(properties => diff --git a/src/Platform/Microsoft.Testing.Platform/TestHostControllers/PassiveNodeDataConsumer.cs b/src/Platform/Microsoft.Testing.Platform/TestHostControllers/PassiveNodeDataConsumer.cs new file mode 100644 index 0000000000..5f95d7325b --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/TestHostControllers/PassiveNodeDataConsumer.cs @@ -0,0 +1,74 @@ +// 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; +using Microsoft.Testing.Platform.Extensions.TestHost; +using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.ServerMode; + +namespace Microsoft.Testing.Platform.TestHostControllers; + +internal class PassiveNodeDataConsumer : IDataConsumer, IDisposable +{ + private const string FileType = "file"; + private readonly PassiveNode? _passiveNode; + + public PassiveNodeDataConsumer(PassiveNode? passiveNode) + => _passiveNode = passiveNode; + + public Type[] DataTypesConsumed + => [typeof(SessionFileArtifact), typeof(TestNodeFileArtifact), typeof(FileArtifact)]; + + public string Uid + => nameof(PassiveNodeDataConsumer); + + public string Version + => AppVersion.DefaultSemVer; + + public string DisplayName + => nameof(PassiveNodeDataConsumer); + + public string Description + => "Push information as passive node"; + + public Task IsEnabledAsync() + => Task.FromResult(_passiveNode is not null); + + public async Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + { + if (_passiveNode is null) + { + return; + } + + switch (value) + { + case TestNodeFileArtifact testNodeFileArtifact: + { + RunTestAttachment runTestAttachment = new(testNodeFileArtifact.FileInfo.FullName, dataProducer.Uid, FileType, testNodeFileArtifact.DisplayName, testNodeFileArtifact.Description); + await _passiveNode.SendAttachmentsAsync(new TestsAttachments([runTestAttachment]), cancellationToken); + break; + } + + case SessionFileArtifact sessionFileArtifact: + { + RunTestAttachment runTestAttachment = new(sessionFileArtifact.FileInfo.FullName, dataProducer.Uid, FileType, sessionFileArtifact.DisplayName, sessionFileArtifact.Description); + await _passiveNode.SendAttachmentsAsync(new TestsAttachments([runTestAttachment]), cancellationToken); + break; + } + + case FileArtifact fileArtifact: + { + RunTestAttachment runTestAttachment = new(fileArtifact.FileInfo.FullName, dataProducer.Uid, FileType, fileArtifact.DisplayName, fileArtifact.Description); + await _passiveNode.SendAttachmentsAsync(new TestsAttachments([runTestAttachment]), cancellationToken); + break; + } + + default: + break; + } + } + + public void Dispose() + => _passiveNode?.Dispose(); +} diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ServerModeTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ServerModeTests.cs index c285617346..9ca1e2813a 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ServerModeTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ServerModeTests.cs @@ -28,7 +28,7 @@ public async Task DiscoverAndRun(string tfm) InitializeResponse initializeResponseArgs = await jsonClient.Initialize(); Assert.IsTrue(initializeResponseArgs.Capabilities.Testing.VSTestProvider); - Assert.IsTrue(initializeResponseArgs.Capabilities.Testing.MultiRequestSupport); + Assert.IsFalse(initializeResponseArgs.Capabilities.Testing.MultiRequestSupport); Assert.IsTrue(initializeResponseArgs.Capabilities.Testing.SupportsDiscovery); TestNodeUpdateCollector discoveryCollector = new(); @@ -57,7 +57,7 @@ public async Task WhenClientDies_Server_ShouldClose_Gracefully(string tfm) jsonClient.RegisterTelemetryListener(telemetry); InitializeResponse initializeResponseArgs = await jsonClient.Initialize(); - Assert.IsTrue(initializeResponseArgs.Capabilities.Testing.MultiRequestSupport); + Assert.IsFalse(initializeResponseArgs.Capabilities.Testing.MultiRequestSupport); TestNodeUpdateCollector discoveryCollector = new(); diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/FormatterUtilitiesTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/FormatterUtilitiesTests.cs index c62ae65727..a20b81d0b4 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/FormatterUtilitiesTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/FormatterUtilitiesTests.cs @@ -206,13 +206,13 @@ private static void AssertSerialize(Type type, string instanceSerialized) if (type == typeof(ServerTestingCapabilities)) { - Assert.AreEqual("""{"supportsDiscovery":true,"experimental_multiRequestSupport":true,"vstestProvider":true}""".Replace(" ", string.Empty), instanceSerialized, because); + Assert.AreEqual("""{"supportsDiscovery":true,"experimental_multiRequestSupport":true,"vstestProvider":true,"attachmentsSupport":true,"multipleConnectionProvider":true}""".Replace(" ", string.Empty), instanceSerialized, because); return; } if (type == typeof(ServerCapabilities)) { - Assert.AreEqual("""{"testing":{"supportsDiscovery":true,"experimental_multiRequestSupport":true,"vstestProvider":true}}""".Replace(" ", string.Empty), instanceSerialized, because); + Assert.AreEqual("""{"testing":{"supportsDiscovery":true,"experimental_multiRequestSupport":true,"vstestProvider":true,"attachmentsSupport":true,"multipleConnectionProvider":true}}""".Replace(" ", string.Empty), instanceSerialized, because); return; } @@ -224,7 +224,7 @@ private static void AssertSerialize(Type type, string instanceSerialized) if (type == typeof(InitializeResponseArgs)) { - Assert.AreEqual("""{"processId":1,"serverInfo":{"name":"ServerInfoName","version":"Version"},"capabilities":{"testing":{"supportsDiscovery":true,"experimental_multiRequestSupport":true,"vstestProvider":true}}}""".Replace(" ", string.Empty), instanceSerialized, because); + Assert.AreEqual("""{"processId":1,"serverInfo":{"name":"ServerInfoName","version":"Version"},"capabilities":{"testing":{"supportsDiscovery":true,"experimental_multiRequestSupport":true,"vstestProvider":true,"attachmentsSupport":true,"multipleConnectionProvider":true}}}""".Replace(" ", string.Empty), instanceSerialized, because); return; } @@ -252,6 +252,18 @@ private static void AssertSerialize(Type type, string instanceSerialized) return; } + if (type == typeof(TestsAttachments)) + { + Assert.AreEqual("""{"attachments":[{"uri":"Uri","producer":"Producer","type":"Type","display-name":"DisplayName","description":"Description"}]}""".Replace(" ", string.Empty), instanceSerialized, because); + return; + } + + if (type == typeof(RunTestAttachment)) + { + Assert.AreEqual("""{"uri":"Uri","producer":"Producer","type":"Type","display-name":"DisplayName","description":"Description"}""".Replace(" ", string.Empty), instanceSerialized, because); + return; + } + if (type == typeof(object)) { Assert.AreEqual("{}".Replace(" ", string.Empty), instanceSerialized, because); @@ -343,14 +355,24 @@ private static object CreateInstance(Type type) return new Artifact("Uri", "Producer", "Type", "DisplayName", "Description"); } + if (type == typeof(TestsAttachments)) + { + return new TestsAttachments(new RunTestAttachment[] { new("Uri", "Producer", "Type", "DisplayName", "Description") }); + } + + if (type == typeof(RunTestAttachment)) + { + return new RunTestAttachment("Uri", "Producer", "Type", "DisplayName", "Description"); + } + if (type == typeof(ServerTestingCapabilities)) { - return new ServerTestingCapabilities(true, true, true); + return new ServerTestingCapabilities(true, true, true, true, true); } if (type == typeof(ServerCapabilities)) { - return new ServerCapabilities(new ServerTestingCapabilities(true, true, true)); + return new ServerCapabilities(new ServerTestingCapabilities(true, true, true, true, true)); } if (type == typeof(ServerInfo)) @@ -360,7 +382,7 @@ private static object CreateInstance(Type type) if (type == typeof(InitializeResponseArgs)) { - return new InitializeResponseArgs(1, new ServerInfo("ServerInfoName", "Version"), new ServerCapabilities(new ServerTestingCapabilities(true, true, true))); + return new InitializeResponseArgs(1, new ServerInfo("ServerInfoName", "Version"), new ServerCapabilities(new ServerTestingCapabilities(true, true, true, true, true))); } if (type == typeof(ErrorMessage)) diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs index 7f6479fdb8..ba16dad5dc 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/ServerTests.cs @@ -115,7 +115,7 @@ await WriteMessageAsync( InitializeResponseArgs expectedResponse = new( 1, new ServerInfo("test-anywhere", "this is dynamic"), - new ServerCapabilities(new ServerTestingCapabilities(SupportsDiscovery: true, MultiRequestSupport: false, VSTestProviderSupport: false))); + new ServerCapabilities(new ServerTestingCapabilities(SupportsDiscovery: true, MultiRequestSupport: false, VSTestProviderSupport: false, SupportsAttachments: true, MultiConnectionProvider: false))); Assert.AreEqual(expectedResponse.Capabilities, resultJson.Capabilities); Assert.AreEqual(expectedResponse.ServerInfo.Name, resultJson.ServerInfo.Name);