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

using Microsoft.VisualStudio.TestPlatform.Common;
using Microsoft.VisualStudio.TestPlatform.Common.Interfaces;
using Microsoft.VisualStudio.TestPlatform.Common.Utilities;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;

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

namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors;

/// <summary>
/// Allows the user to specify a order of loading custom adapters from.
/// </summary>
internal class TestAdapterLoadingStrategyArgumentProcessor : IArgumentProcessor
{
    /// <summary>
    /// The name of the command line argument that the TestAdapterLoadingStrategyArgumentProcessor handles.
    /// </summary>
    public const string CommandName = "/TestAdapterLoadingStrategy";

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

    /// <summary>
    /// Gets the metadata.
    /// </summary>
    public Lazy<IArgumentProcessorCapabilities> Metadata
        => _metadata ??= new Lazy<IArgumentProcessorCapabilities>(() =>
            new TestAdapterLoadingStrategyArgumentProcessorCapabilities());

    /// <summary>
    /// Gets or sets the executor.
    /// </summary>
    public Lazy<IArgumentExecutor>? Executor
    {
        get => _executor ??= new Lazy<IArgumentExecutor>(() =>
            new TestAdapterLoadingStrategyArgumentExecutor(CommandLineOptions.Instance, RunSettingsManager.Instance, ConsoleOutput.Instance, new FileHelper()));

        set => _executor = value;
    }
}

/// <summary>
/// The argument capabilities.
/// </summary>
internal class TestAdapterLoadingStrategyArgumentProcessorCapabilities : BaseArgumentProcessorCapabilities
{
    public override string CommandName => TestAdapterLoadingStrategyArgumentProcessor.CommandName;

    public override bool AllowMultiple => false;

    public override bool IsAction => false;

    public override bool AlwaysExecute => true;

    public override ArgumentProcessorPriority Priority => ArgumentProcessorPriority.TestAdapterLoadingStrategy;

    public override string HelpContentResourceName => CommandLineResources.TestAdapterLoadingStrategyHelp;

    public override HelpContentPriority HelpPriority => HelpContentPriority.TestAdapterLoadingStrategyArgumentProcessorHelpPriority;
}

/// <summary>
/// The argument executor.
/// </summary>
internal class TestAdapterLoadingStrategyArgumentExecutor : IArgumentExecutor
{
    /// <summary>
    /// Used for getting sources.
    /// </summary>
    private readonly CommandLineOptions _commandLineOptions;

    /// <summary>
    /// Run settings provider.
    /// </summary>
    private readonly IRunSettingsProvider _runSettingsManager;

    /// <summary>
    /// Used for sending output.
    /// </summary>
    private readonly IOutput _output;

    /// <summary>
    /// For file related operation
    /// </summary>
    private readonly IFileHelper _fileHelper;

    public const string RunSettingsPath = "RunConfiguration.TestAdapterLoadingStrategy";

    /// <summary>
    /// Default constructor.
    /// </summary>
    /// <param name="options"> The options. </param>
    /// <param name="runSettingsManager">Run setting manager.</param>
    /// <param name="output">Output such as console.</param>
    /// <param name="fileHelper">File helper</param>
    public TestAdapterLoadingStrategyArgumentExecutor(CommandLineOptions options, IRunSettingsProvider runSettingsManager, IOutput output, IFileHelper fileHelper)
    {
        _commandLineOptions = options ?? throw new ArgumentNullException(nameof(options));
        _runSettingsManager = runSettingsManager ?? throw new ArgumentNullException(nameof(runSettingsManager));
        _output = output ?? throw new ArgumentNullException(nameof(output));
        _fileHelper = fileHelper ?? throw new ArgumentNullException(nameof(fileHelper));
    }

    #region IArgumentExecutor
    /// <summary>
    /// Initializes with the argument that was provided with the command.
    /// </summary>
    /// <param name="argument">Argument that was provided with the command.</param>
    public void Initialize(string? argument)
    {
        ExtractStrategy(argument, out var strategy);

        if (strategy == TestAdapterLoadingStrategy.Recursive)
        {
            throw new CommandLineException(string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestAdapterLoadingStrategyValueInvalidRecursive, $"{nameof(TestAdapterLoadingStrategy.Explicit)}, {nameof(TestAdapterLoadingStrategy.NextToSource)}"));
        }

        if (strategy == TestAdapterLoadingStrategy.Default)
        {
            InitializeDefaultStrategy();
            return;
        }

        InitializeStrategy(strategy);
    }

    /// <summary>
    /// Executes the argument processor.
    /// </summary>
    /// <returns> The <see cref="ArgumentProcessorResult"/>. </returns>
    public ArgumentProcessorResult Execute()
    {
        // Nothing to do since we updated the parameter during initialize parameter
        return ArgumentProcessorResult.Success;
    }
    #endregion

    private void ExtractStrategy(string? value, out TestAdapterLoadingStrategy strategy)
    {
        value ??= _runSettingsManager.QueryRunSettingsNode(RunSettingsPath);

        if (value.IsNullOrWhiteSpace())
        {
            strategy = TestAdapterLoadingStrategy.Default;
            return;
        }

        if (!Enum.TryParse(value, out strategy))
        {
            throw new CommandLineException(string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestAdapterLoadingStrategyValueInvalid, value));
        }
    }

    private void InitializeDefaultStrategy()
    {
        ValidateTestAdapterPaths(TestAdapterLoadingStrategy.Default);

        SetStrategy(TestAdapterLoadingStrategy.Default);
    }

    private void InitializeStrategy(TestAdapterLoadingStrategy strategy)
    {
        ValidateTestAdapterPaths(strategy);

        if (!_commandLineOptions.TestAdapterPathsSet && strategy.HasFlag(TestAdapterLoadingStrategy.Explicit))
        {
            throw new CommandLineException(string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestAdapterPathValueRequiredWhenStrategyXIsUsed, nameof(TestAdapterLoadingStrategy.Explicit)));
        }

        SetStrategy(strategy);
    }

    private void ForceIsolation()
    {
        if (_commandLineOptions.InIsolation)
        {
            return;
        }

        EqtTrace.Warning(
            $"{nameof(TestAdapterLoadingStrategyArgumentExecutor)}.{nameof(ForceIsolation)}: InIsolation setting is forced when {nameof(TestAdapterLoadingStrategy.Explicit)} strategy is used." +
            "Tests will run in isolation."
        );
        _commandLineOptions.InIsolation = true;
        _runSettingsManager.UpdateRunSettingsNode(InIsolationArgumentExecutor.RunSettingsPath, "true");
    }

    private void ValidateTestAdapterPaths(TestAdapterLoadingStrategy strategy)
    {
        var testAdapterPaths = _commandLineOptions.TestAdapterPath ?? [];
        if (!_commandLineOptions.TestAdapterPathsSet)
        {
            testAdapterPaths = TestAdapterPathArgumentExecutor.SplitPaths(_runSettingsManager.QueryRunSettingsNode(TestAdapterPathArgumentExecutor.RunSettingsPath)).Union(testAdapterPaths).Distinct().ToArray();
        }

        for (var i = 0; i < testAdapterPaths.Length; i++)
        {
            var adapterPath = testAdapterPaths[i];
            var testAdapterPath = _fileHelper.GetFullPath(Environment.ExpandEnvironmentVariables(adapterPath));

            if (strategy == TestAdapterLoadingStrategy.Default && !_fileHelper.DirectoryExists(testAdapterPath))
            {
                throw new CommandLineException(
                    string.Format(CultureInfo.CurrentCulture, CommandLineResources.InvalidTestAdapterPathCommand, adapterPath, CommandLineResources.TestAdapterPathDoesNotExist)
                );
            }

            testAdapterPaths[i] = testAdapterPath;
        }

        _runSettingsManager.UpdateRunSettingsNode(TestAdapterPathArgumentExecutor.RunSettingsPath, string.Join(";", testAdapterPaths));
    }

    private void SetStrategy(TestAdapterLoadingStrategy strategy)
    {
        _commandLineOptions.TestAdapterLoadingStrategy = strategy;
        _runSettingsManager.UpdateRunSettingsNode(RunSettingsPath, strategy.ToString());
        if (strategy.HasFlag(TestAdapterLoadingStrategy.Explicit))
        {
            ForceIsolation();
        }
    }
}