File: Processors\RunSpecificTestsArgumentProcessor.cs
Web Access
Project: src\src\vstest\src\vstest.console\vstest.console.csproj (vstest.console)
// 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.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;

using Microsoft.VisualStudio.TestPlatform.Client.RequestHelper;
using Microsoft.VisualStudio.TestPlatform.CommandLine.Internal;
using Microsoft.VisualStudio.TestPlatform.CommandLine.TestPlatformHelpers;
using Microsoft.VisualStudio.TestPlatform.Common;
using Microsoft.VisualStudio.TestPlatform.Common.Interfaces;
using Microsoft.VisualStudio.TestPlatform.Common.Utilities;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.ArtifactProcessing;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine;
using Microsoft.VisualStudio.TestPlatform.Utilities;

using CommandLineResources = Microsoft.VisualStudio.TestPlatform.CommandLine.Resources.Resources;

namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors;

internal class RunSpecificTestsArgumentProcessor : IArgumentProcessor
{
    public const string CommandName = "/Tests";

    private Lazy<IArgumentProcessorCapabilities>? _metadata;
    private Lazy<IArgumentExecutor>? _executor;

    public Lazy<IArgumentProcessorCapabilities> Metadata
        => _metadata ??= new Lazy<IArgumentProcessorCapabilities>(() =>
            new RunSpecificTestsArgumentProcessorCapabilities());

    public Lazy<IArgumentExecutor>? Executor
    {
        get => _executor ??= new Lazy<IArgumentExecutor>(() =>
            new RunSpecificTestsArgumentExecutor(
                CommandLineOptions.Instance,
                RunSettingsManager.Instance,
                TestRequestManager.Instance,
                new ArtifactProcessingManager(CommandLineOptions.Instance.TestSessionCorrelationId),
                ConsoleOutput.Instance));

        set => _executor = value;
    }
}

internal class RunSpecificTestsArgumentProcessorCapabilities : BaseArgumentProcessorCapabilities
{
    public override string CommandName => RunSpecificTestsArgumentProcessor.CommandName;

    public override bool IsAction => true;

    public override bool AllowMultiple => false;

    public override string HelpContentResourceName => CommandLineResources.RunSpecificTestsHelp;

    public override HelpContentPriority HelpPriority => HelpContentPriority.RunSpecificTestsArgumentProcessorHelpPriority;

    public override ArgumentProcessorPriority Priority => ArgumentProcessorPriority.Normal;
}

internal class RunSpecificTestsArgumentExecutor : IArgumentExecutor
{
    public const char SplitDelimiter = ',';
    public const char EscapeDelimiter = '\\';

    /// <summary>
    /// Used for getting sources.
    /// </summary>
    private readonly CommandLineOptions _commandLineOptions;

    /// <summary>
    /// The instance of testPlatforms
    /// </summary>
    private readonly ITestRequestManager _testRequestManager;

    /// <summary>
    /// Used for sending output.
    /// </summary>
    internal IOutput Output;

    /// <summary>
    /// RunSettingsManager to get currently active run settings.
    /// </summary>
    private readonly IRunSettingsProvider _runSettingsManager;

    /// <summary>
    /// Given Collection of strings for filtering test cases
    /// </summary>
    private Collection<string>? _selectedTestNames;

    /// <summary>
    /// Used for tracking the total no. of tests discovered from the given sources.
    /// </summary>
    private long _discoveredTestCount;

    /// <summary>
    /// Collection of test cases that match at least one of the given search strings
    /// </summary>
    private readonly Collection<TestCase> _selectedTestCases = new();

    /// <summary>
    /// Effective run settings applicable to test run after inferring the multi-targeting settings.
    /// </summary>
    private string? _effectiveRunSettings;

    /// <summary>
    /// List of filters that have not yet been discovered
    /// </summary>
    HashSet<string> _undiscoveredFilters = new();

    /// <summary>
    /// Registers for discovery events during discovery
    /// </summary>
    private readonly ITestDiscoveryEventsRegistrar _discoveryEventsRegistrar;

    /// <summary>
    /// Registers and Unregisters for test run events before and after test run
    /// </summary>
    private readonly ITestRunEventsRegistrar _testRunEventsRegistrar;

    /// <summary>
    /// Default constructor.
    /// </summary>
    public RunSpecificTestsArgumentExecutor(
        CommandLineOptions options,
        IRunSettingsProvider runSettingsProvider,
        ITestRequestManager testRequestManager,
        IArtifactProcessingManager artifactProcessingManager,
        IOutput output)
    {
        ValidateArg.NotNull(options, nameof(options));
        ValidateArg.NotNull(testRequestManager, nameof(testRequestManager));

        _commandLineOptions = options;
        _testRequestManager = testRequestManager;

        _runSettingsManager = runSettingsProvider;
        Output = output;
        _discoveryEventsRegistrar = new DiscoveryEventsRegistrar(DiscoveryRequest_OnDiscoveredTests);
        _testRunEventsRegistrar = new TestRunRequestEventsRegistrar(Output, _commandLineOptions, artifactProcessingManager);
    }

    #region IArgumentProcessor

    /// <summary>
    /// Splits given the search strings and adds to selectTestNamesCollection.
    /// </summary>
    /// <param name="argument"></param>
    [MemberNotNull(nameof(_selectedTestNames))]
    public void Initialize(string? argument)
    {
        if (!argument.IsNullOrWhiteSpace())
        {
            _selectedTestNames = new Collection<string>(
                argument.Tokenize(SplitDelimiter, EscapeDelimiter)
                    .Where(x => !StringUtils.IsNullOrWhiteSpace(x))
                    .Select(s => s.Trim()).ToList());
        }

        if (_selectedTestNames == null || _selectedTestNames.Count <= 0)
        {
            throw new CommandLineException(CommandLineResources.SpecificTestsRequired);
        }

        // by default all filters are not discovered on launch
        _undiscoveredFilters = new HashSet<string>(_selectedTestNames);
    }

    /// <summary>
    /// Execute specific tests that match any of the given strings.
    /// </summary>
    /// <returns></returns>
    public ArgumentProcessorResult Execute()
    {
        TPDebug.Assert(Output != null);
        TPDebug.Assert(_commandLineOptions != null);
        TPDebug.Assert(_testRequestManager != null);
        TPDebug.Assert(!StringUtils.IsNullOrWhiteSpace(_runSettingsManager.ActiveRunSettings?.SettingsXml));

        if (!_commandLineOptions.Sources.Any())
        {
            throw new CommandLineException(CommandLineResources.MissingTestSourceFile);
        }

        _effectiveRunSettings = _runSettingsManager.ActiveRunSettings.SettingsXml;

        // Discover tests from sources and filter on every discovery reported.
        DiscoverTestsAndSelectSpecified(_commandLineOptions.Sources);

        // Now that tests are discovered and filtered, we run only those selected tests.
        ExecuteSelectedTests();

        bool treatNoTestsAsError = RunSettingsUtilities.GetTreatNoTestsAsError(_effectiveRunSettings);

        return treatNoTestsAsError && _selectedTestCases.Count == 0 ? ArgumentProcessorResult.Fail : ArgumentProcessorResult.Success;
    }

    #endregion

    /// <summary>
    /// Discovers tests from the given sources and selects only specified tests.
    /// </summary>
    /// <param name="sources"> Test source assemblies paths. </param>
    private void DiscoverTestsAndSelectSpecified(IEnumerable<string> sources)
    {
        if (Environment.GetEnvironmentVariable("VSTEST_MSBUILD_NOLOGO") != "1")
        {
            Output.WriteLine(CommandLineResources.StartingDiscovery, OutputLevel.Information);
        }

        if (!StringUtils.IsNullOrEmpty(EqtTrace.LogFile))
        {
            Output.Information(false, CommandLineResources.VstestDiagLogOutputPath, EqtTrace.LogFile);
        }

        _testRequestManager.DiscoverTests(
            new DiscoveryRequestPayload() { Sources = sources, RunSettings = _effectiveRunSettings }, _discoveryEventsRegistrar, Constants.DefaultProtocolConfig);
    }

    /// <summary>
    ///  Executes the selected tests
    /// </summary>
    private void ExecuteSelectedTests()
    {
        if (_selectedTestCases.Count > 0)
        {
            if (_undiscoveredFilters.Count != 0)
            {
                string missingFilters = string.Join(", ", _undiscoveredFilters);
                string warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.SomeTestsUnavailableAfterFiltering, _discoveredTestCount, missingFilters);
                Output.Warning(false, warningMessage);
            }

            // for command line keep alive is always false.
            bool keepAlive = false;

            EqtTrace.Verbose("RunSpecificTestsArgumentProcessor:Execute: Test run is queued.");
            var runRequestPayload = new TestRunRequestPayload() { TestCases = _selectedTestCases.ToList(), RunSettings = _effectiveRunSettings, KeepAlive = keepAlive, TestPlatformOptions = new TestPlatformOptions() { TestCaseFilter = _commandLineOptions.TestCaseFilterValue } };
            _testRequestManager.RunTests(runRequestPayload, null, _testRunEventsRegistrar, Constants.DefaultProtocolConfig);
        }
        else
        {
            string warningMessage;
            if (_discoveredTestCount > 0)
            {
                // No tests that matched any of the given strings.
                warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.NoTestsAvailableAfterFiltering, _discoveredTestCount, string.Join(", ", _selectedTestNames!));
            }
            else
            {
                // No tests were discovered from the given sources.
                warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.NoTestsAvailableInSources, string.Join(", ", _commandLineOptions.Sources));

                if (!_commandLineOptions.TestAdapterPathsSet)
                {
                    warningMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.StringFormatToJoinTwoStrings, warningMessage, CommandLineResources.SuggestTestAdapterPathIfNoTestsIsFound);
                }
            }

            Output.Warning(false, warningMessage);
        }
    }

    /// <summary>
    /// Filter discovered tests and find matching tests from given search strings.
    /// Any name of the test that can match multiple strings will be added only once.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    private void DiscoveryRequest_OnDiscoveredTests(object? sender, DiscoveredTestsEventArgs args)
    {
        TPDebug.Assert(_selectedTestNames != null, "Initialize should have been called");
        TPDebug.Assert(args.DiscoveredTestCases != null);

        _discoveredTestCount += args.DiscoveredTestCases.Count();
        foreach (var testCase in args.DiscoveredTestCases!)
        {
            foreach (var nameCriteria in _selectedTestNames)
            {
                if (testCase.FullyQualifiedName.IndexOf(nameCriteria, StringComparison.OrdinalIgnoreCase) != -1)
                {
                    _selectedTestCases.Add(testCase);

                    // If a testcase matched then a filter matched - so remove the filter from not found list
                    _undiscoveredFilters.Remove(nameCriteria);
                    break;
                }
            }
        }
    }

    private class DiscoveryEventsRegistrar : ITestDiscoveryEventsRegistrar
    {
        private readonly EventHandler<DiscoveredTestsEventArgs> _discoveredTestsHandler;

        public DiscoveryEventsRegistrar(EventHandler<DiscoveredTestsEventArgs> discoveredTestsHandler)
        {
            _discoveredTestsHandler = discoveredTestsHandler;
        }

        public void LogWarning(string message)
        {
            ConsoleLogger.RaiseTestRunWarning(message);
        }

        public void RegisterDiscoveryEvents(IDiscoveryRequest discoveryRequest)
        {
            discoveryRequest.OnDiscoveredTests += _discoveredTestsHandler;
        }

        public void UnregisterDiscoveryEvents(IDiscoveryRequest discoveryRequest)
        {
            discoveryRequest.OnDiscoveredTests -= _discoveredTestsHandler;
        }
    }

    private class TestRunRequestEventsRegistrar : ITestRunEventsRegistrar
    {
        private readonly IOutput _output;
        private readonly CommandLineOptions _commandLineOptions;
        private readonly IArtifactProcessingManager _artifactProcessingManager;

        public TestRunRequestEventsRegistrar(IOutput output, CommandLineOptions commandLineOptions, IArtifactProcessingManager artifactProcessingManager)
        {
            _output = output;
            _commandLineOptions = commandLineOptions;
            _artifactProcessingManager = artifactProcessingManager;
        }

        public void LogWarning(string message)
        {
            ConsoleLogger.RaiseTestRunWarning(message);
        }

        public void RegisterTestRunEvents(ITestRunRequest testRunRequest)
        {
            testRunRequest.OnRunCompletion += TestRunRequest_OnRunCompletion;
        }

        public void UnregisterTestRunEvents(ITestRunRequest testRunRequest)
        {
            testRunRequest.OnRunCompletion -= TestRunRequest_OnRunCompletion;
        }

        /// <summary>
        /// Handles the TestRunRequest complete event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e">RunCompletion args</param>
        private void TestRunRequest_OnRunCompletion(object? sender, TestRunCompleteEventArgs e)
        {
            // If run is not aborted/canceled then check the count of executed tests.
            // we need to check if there are any tests executed - to try show some help info to user to check for installed vsix extensions
            if (!e.IsAborted && !e.IsCanceled)
            {
                var testsFoundInAnySource = e.TestRunStatistics != null && (e.TestRunStatistics.ExecutedTests > 0);

                // Indicate the user to use testadapterpath command if there are no tests found
                if (!testsFoundInAnySource && !CommandLineOptions.Instance.TestAdapterPathsSet && _commandLineOptions.TestCaseFilterValue == null)
                {
                    _output.Warning(false, CommandLineResources.SuggestTestAdapterPathIfNoTestsIsFound);
                }
            }

            // Collect tests session artifacts for post processing
            if (_commandLineOptions.ArtifactProcessingMode == ArtifactProcessingMode.Collect)
            {
                TPDebug.Assert(RunSettingsManager.Instance.ActiveRunSettings.SettingsXml is not null, "RunSettingsManager.Instance.ActiveRunSettings.SettingsXml is null");
                _artifactProcessingManager.CollectArtifacts(e, RunSettingsManager.Instance.ActiveRunSettings.SettingsXml);
            }
        }
    }
}