Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PassiveNode for out of process attachments. #3865

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions samples/Playground/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -25,6 +28,9 @@ public static async Task<int> 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();

Expand Down Expand Up @@ -56,3 +62,38 @@ public static async Task<int> 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<bool> 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;
}
4 changes: 1 addition & 3 deletions samples/Playground/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <inheritdoc />
Expand All @@ -25,5 +24,5 @@ public sealed class VSTestBridgeExtensionBaseCapabilities : ITrxReportCapability
/// <inheritdoc />
void ITrxReportCapability.Enable() => IsTrxEnabled = true;

bool INamedFeatureCapability.IsSupported(string featureName) => featureName is MultiRequestSupport or VSTestProviderSupport;
bool INamedFeatureCapability.IsSupported(string featureName) => featureName is VSTestProviderSupport;
}
17 changes: 10 additions & 7 deletions src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestFrameworkBuilderData, Task<ITestFramework>> _buildTestFrameworkAsync;

private readonly IMessageHandlerFactory _messageHandlerFactory;
Expand Down Expand Up @@ -389,8 +389,11 @@ private async Task<object> 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):
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PassiveNode>());
}

// Clone the service provider to avoid to add the message bus proxy to the main service provider.
var testHostControllersServiceProvider = (ServiceProvider)serviceProvider.Clone();

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -124,21 +126,34 @@ protected override async Task<int> InternalRunAsync()

List<IDataConsumer> dataConsumersBuilder = [.. _testHostsInformation.DataConsumer];

// We add the IPlatformOutputDevice after all users extensions.
IPlatformOutputDevice? display = ServiceProvider.GetServiceInternal<IPlatformOutputDevice>();
if (display is IDataConsumer dataConsumerDisplay)
{
dataConsumersBuilder.Add(dataConsumerDisplay);
}

IPushOnlyProtocol? pushOnlyProtocol = ServiceProvider.GetService<IPushOnlyProtocol>();
// We register the DotnetTestDataConsumer as last to ensure that it will be the last one to consume the data.
IPushOnlyProtocol? pushOnlyProtocol = ServiceProvider.GetService<IPushOnlyProtocol>();
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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public Json(Dictionary<Type, JsonSerializer>? serializers = null, Dictionary<Typ
(JsonRpcStrings.SupportsDiscovery, capabilities.SupportsDiscovery),
(JsonRpcStrings.MultiRequestSupport, capabilities.MultiRequestSupport),
(JsonRpcStrings.VSTestProviderSupport, capabilities.VSTestProviderSupport),
(JsonRpcStrings.AttachmentsSupport, capabilities.SupportsAttachments),
(JsonRpcStrings.MultiConnectionProvider, capabilities.MultiConnectionProvider),
});

_serializers[typeof(Artifact)] = new JsonObjectSerializer<Artifact>(artifact =>
Expand Down Expand Up @@ -334,6 +336,22 @@ public Json(Dictionary<Type, JsonSerializer>? serializers = null, Dictionary<Typ
(JsonRpcStrings.ProcessId, info.ProcessId),
});

_serializers[typeof(TestsAttachments)] = new JsonObjectSerializer<TestsAttachments>(info =>
new (string, object?)[]
{
(JsonRpcStrings.Attachments, info.Attachments),
});

_serializers[typeof(RunTestAttachment)] = new JsonObjectSerializer<RunTestAttachment>(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<string>((w, v) => w.WriteStringValue(v));
_serializers[typeof(bool)] = new JsonValueSerializer<bool>((w, v) => w.WriteBooleanValue(v));
Expand Down Expand Up @@ -474,7 +492,9 @@ public Json(Dictionary<Type, JsonSerializer>? serializers = null, Dictionary<Typ
(json, jsonElement) => new ServerTestingCapabilities(
SupportsDiscovery: json.Bind<bool>(jsonElement, JsonRpcStrings.SupportsDiscovery),
MultiRequestSupport: json.Bind<bool>(jsonElement, JsonRpcStrings.MultiRequestSupport),
VSTestProviderSupport: json.Bind<bool>(jsonElement, JsonRpcStrings.VSTestProviderSupport)));
VSTestProviderSupport: json.Bind<bool>(jsonElement, JsonRpcStrings.VSTestProviderSupport),
SupportsAttachments: json.Bind<bool>(jsonElement, JsonRpcStrings.AttachmentsSupport),
MultiConnectionProvider: json.Bind<bool>(jsonElement, JsonRpcStrings.MultiConnectionProvider)));

_deserializers[typeof(DiscoverRequestArgs)] = new JsonElementDeserializer<DiscoverRequestArgs>((json, jsonElement) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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";
Expand Down
Loading