File: Hosting\MtpTestRuntimeProvider.cs
Web Access
Project: src\vstest\src\Microsoft.TestPlatform.TestHostProvider\Microsoft.TestPlatform.TestHostProvider.csproj (Microsoft.TestPlatform.TestHostRuntimeProvider)
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.VisualStudio.TestPlatform.CoreUtilities.Helpers;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client.MTP;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.DataCollection.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Host;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;

namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Hosting;

/// <summary>
/// Runtime provider for Microsoft.Testing.Platform (MTP) test applications.
/// </summary>
/// <remarks>
/// An MTP test application is its own test host: it is launched in server mode and driven directly over the
/// Microsoft.Testing.Platform JSON-RPC protocol, not the vstest testhost protocol. This provider therefore does
/// not launch a vstest testhost at all — the launch-related members of <see cref="ITestRuntimeProvider"/> throw
/// <see cref="NotSupportedException"/>. Instead it:
/// <list type="bullet">
///   <item><description>
///     claims MTP sources via <see cref="ISourceAwareTestRuntimeProvider"/> (source-aware detection using the
///     build-time Microsoft.Testing.Platform marker), so it is selected ahead of the generic testhost providers
///     that match only by target framework; and
///   </description></item>
///   <item><description>
///     supplies its own discovery/execution proxy managers via <see cref="IProxyManagerFactory"/>, so the
///     engine drives MTP sources over the MTP proxies without any protocol-specific branching in the engine.
///   </description></item>
/// </list>
/// This is why the provider is registered like a normal runtime provider yet routes to
/// <see cref="MtpProxyDiscoveryManager"/>/<see cref="MtpProxyExecutionManager"/> instead of a vstest testhost.
/// </remarks>
[ExtensionUri(MicrosoftTestingPlatformHostUri)]
[FriendlyName(MicrosoftTestingPlatformHostFriendlyName)]
public class MtpTestRuntimeProvider : ISourceAwareTestRuntimeProvider, IProxyManagerFactory
{
    private const string MicrosoftTestingPlatformHostUri = "HostProvider://MicrosoftTestingPlatformHost";
    private const string MicrosoftTestingPlatformHostFriendlyName = "MicrosoftTestingPlatformHost";

    // The launch-related members are not supported because an MTP application hosts itself; it is never
    // launched as a vstest testhost. Discovery and execution go through the MTP proxy managers instead.
    private const string NotSupportedMessage = "Microsoft.Testing.Platform applications are hosted over the MTP protocol and are not launched as a vstest test host.";

    event EventHandler<HostProviderEventArgs>? ITestRuntimeProvider.HostLaunched
    {
        add { }
        remove { }
    }

    event EventHandler<HostProviderEventArgs>? ITestRuntimeProvider.HostExited
    {
        add { }
        remove { }
    }

    bool ITestRuntimeProvider.Shared => false;

    void ITestRuntimeProvider.Initialize(IMessageLogger? logger, string runsettingsXml)
    {
    }

    // Source-blind resolution can never own a run: without the sources we cannot tell whether they are MTP
    // applications, so we decline and let the source-aware path below make the decision.
    bool ITestRuntimeProvider.CanExecuteCurrentRunConfiguration(string? runsettingsXml) => false;

    // Source-aware resolution: this provider owns the run only when every source is a Microsoft.Testing.Platform
    // application. A mixed set (some MTP, some classic) is split into separate configurations upstream, so each
    // group asked here is homogeneous.
    bool ISourceAwareTestRuntimeProvider.CanExecuteCurrentRunConfiguration(string? runsettingsXml, IEnumerable<string> sources)
        => AllSourcesAreMicrosoftTestingPlatform(sources);

    void ITestRuntimeProvider.SetCustomLauncher(ITestHostLauncher customLauncher)
    {
    }

    TestHostConnectionInfo ITestRuntimeProvider.GetTestHostConnectionInfo() => throw new NotSupportedException(NotSupportedMessage);

    Task<bool> ITestRuntimeProvider.LaunchTestHostAsync(TestProcessStartInfo testHostStartInfo, CancellationToken cancellationToken) => throw new NotSupportedException(NotSupportedMessage);

    TestProcessStartInfo ITestRuntimeProvider.GetTestHostProcessStartInfo(IEnumerable<string> sources, IDictionary<string, string?>? environmentVariables, TestRunnerConnectionInfo connectionInfo) => throw new NotSupportedException(NotSupportedMessage);

    IEnumerable<string> ITestRuntimeProvider.GetTestPlatformExtensions(IEnumerable<string> sources, IEnumerable<string> extensions) => Enumerable.Empty<string>();

    IEnumerable<string> ITestRuntimeProvider.GetTestSources(IEnumerable<string> sources) => sources;

    Task ITestRuntimeProvider.CleanTestHostAsync(CancellationToken cancellationToken) => Task.CompletedTask;

    IProxyDiscoveryManager IProxyManagerFactory.CreateDiscoveryManager() => MtpProxyManagerFactory.CreateDiscoveryManager();

    IProxyExecutionManager IProxyManagerFactory.CreateExecutionManager(IProxyDataCollectionManager? dataCollectionManager)
        => MtpProxyManagerFactory.CreateExecutionManager(dataCollectionManager);

    private static bool AllSourcesAreMicrosoftTestingPlatform(IEnumerable<string> sources)
    {
        var any = false;
        foreach (var source in sources)
        {
            any = true;

            // The detector never throws: for a null/empty/unreadable source it returns false, which correctly
            // disqualifies the group.
            if (!MicrosoftTestingPlatformDetector.IsMicrosoftTestingPlatformApp(source))
            {
                return false;
            }
        }

        return any;
    }
}