File: Tasks\TestTaskUtils.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.Build\Microsoft.TestPlatform.Build.csproj (Microsoft.TestPlatform.Build)
// 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.IO;

using Microsoft.Build.Utilities;

namespace Microsoft.TestPlatform.Build.Tasks;

internal static class TestTaskUtils
{
    public static string CreateCommandLineArguments(ITestTask task)
    {
        const string codeCoverageString = "Code Coverage";
        const string vsTestAppName = "vstest.console.dll";

        // We have 2 tasks:
        // VSTestTask that writes directly to console output and does not write via msbuild
        // and VSTestTask2 that uses msbuild tools task to capture standard output and translates
        // all output to Error to msbuild errors.
        //
        // Here we are choosing a vstest.console logger to use. Those loggers both write to standard output
        // and error output in vstest.console process, but MSBuild task is not focusing on writing with nice colors
        // but instead on writing cohesive messages in one go, so MSBuild can capture them as single message, and not
        // as 20 different messages for 20 lines.
        var loggerToUse = task is VSTestTask ? "Console" : "Microsoft.TestPlatform.MSBuildLogger";
        // User is free to override the logging using the standard vstest.parameters
        // otherwise we just add the appropriate logger and use the msbuild verbosity
        // to forward it to the vstest.console logger.
        var isLoggerSpecifiedByUser = false;
        var isCollectCodeCoverageEnabled = false;
        var isRunSettingsEnabled = false;

        var builder = new CommandLineBuilder();
        builder.AppendSwitch("exec");
        if (task.VSTestConsolePath != null && !task.VSTestConsolePath.ItemSpec.IsNullOrEmpty())
        {
            builder.AppendFileNameIfNotNull(task.VSTestConsolePath);
        }
        else
        {
            builder.AppendFileNameIfNotNull(vsTestAppName);
        }

        // TODO log arguments in task
        if (!task.VSTestSetting.IsNullOrEmpty())
        {
            isRunSettingsEnabled = true;
            builder.AppendSwitchIfNotNull("--settings:", task.VSTestSetting);
        }

        if (task.VSTestTestAdapterPath != null)
        {
            foreach (var arg in task.VSTestTestAdapterPath)
            {
                builder.AppendSwitchIfNotNull("--testAdapterPath:", arg);
            }
        }

        builder.AppendSwitchIfNotNull("--framework:", task.VSTestFramework);

        // vstest.console only support x86 and x64 for argument platform
        if (!task.VSTestPlatform.IsNullOrEmpty() && !task.VSTestPlatform.Contains("AnyCPU"))
        {
            builder.AppendSwitchIfNotNull("--platform:", task.VSTestPlatform);
        }

        builder.AppendSwitchIfNotNull("--testCaseFilter:", task.VSTestTestCaseFilter);

        if (task.VSTestLogger != null)
        {
            foreach (var arg in task.VSTestLogger)
            {
                builder.AppendSwitchIfNotNull("--logger:", arg);

                if (arg != null && arg.StartsWith(loggerToUse, StringComparison.OrdinalIgnoreCase))
                {
                    System.Diagnostics.Trace.WriteLine($">>>> : User specified logger: {arg}");
                    isLoggerSpecifiedByUser = true;
                }
            }
        }

        builder.AppendSwitchIfNotNull("--resultsDirectory:", task.VSTestResultsDirectory);

        if (task.VSTestListTests)
        {
            builder.AppendSwitch("--listTests");
        }

        builder.AppendSwitchIfNotNull("--diag:", task.VSTestDiag);

        if (task.TestFileFullPath == null || task.TestFileFullPath.ItemSpec.IsNullOrEmpty())
        {
            task.Log.LogError(Resources.Resources.TestFilePathCannotBeEmptyOrNull);
        }
        else
        {
            builder.AppendFileNameIfNotNull(task.TestFileFullPath);
        }

        // The logger to use (console or msbuild) logger was not specified by user,
        // add the logger and verbosity, so we know what to use in vstest.console.
        if (!isLoggerSpecifiedByUser)
        {
            string vsTestVerbosity = "minimal";
            if (!task.VSTestVerbosity.IsNullOrWhiteSpace())
            {
                var normalTestLogging = new List<string>() { "n", "normal", "d", "detailed", "diag", "diagnostic" };
                var quietTestLogging = new List<string>() { "q", "quiet" };

                string taskVsTestVerbosity = task.VSTestVerbosity.ToLowerInvariant();
                if (normalTestLogging.Contains(taskVsTestVerbosity))
                {
                    vsTestVerbosity = "normal";
                }
                else if (quietTestLogging.Contains(taskVsTestVerbosity))
                {
                    vsTestVerbosity = "quiet";
                }
            }

            builder.AppendSwitchUnquotedIfNotNull("--logger:", $"{loggerToUse};Verbosity={vsTestVerbosity}");
        }

        if (task.VSTestBlame || task.VSTestBlameCrash || task.VSTestBlameHang)
        {
            var dumpArgs = new List<string>();
            if (task.VSTestBlameCrash || task.VSTestBlameHang)
            {
                if (task.VSTestBlameCrash)
                {
                    dumpArgs.Add("CollectDump");
                    if (task.VSTestBlameCrashCollectAlways)
                    {
                        dumpArgs.Add($"CollectAlways={task.VSTestBlameCrashCollectAlways}");
                    }

                    if (!task.VSTestBlameCrashDumpType.IsNullOrEmpty())
                    {
                        dumpArgs.Add($"DumpType={task.VSTestBlameCrashDumpType}");
                    }
                }

                if (task.VSTestBlameHang)
                {
                    dumpArgs.Add("CollectHangDump");

                    if (!task.VSTestBlameHangDumpType.IsNullOrEmpty())
                    {
                        dumpArgs.Add($"HangDumpType={task.VSTestBlameHangDumpType}");
                    }

                    if (!task.VSTestBlameHangTimeout.IsNullOrEmpty())
                    {
                        dumpArgs.Add($"TestTimeout={task.VSTestBlameHangTimeout}");
                    }
                }
            }

            if (dumpArgs.Count != 0)
            {
                builder.AppendSwitchIfNotNull("--Blame:", string.Join(";", dumpArgs));
            }
            else
            {
                builder.AppendSwitch("--Blame");
            }
        }

        if (task.VSTestCollect != null)
        {
            foreach (var arg in task.VSTestCollect)
            {
                if (arg == null)
                {
                    continue;
                }

                // For collecting code coverage, argument value can be either "Code Coverage" or "Code Coverage;a=b;c=d".
                // Split the argument with ';' and compare first token value.
                var tokens = arg.Split(';');
                if (arg.Equals(codeCoverageString, StringComparison.OrdinalIgnoreCase) ||
                    tokens[0].Equals(codeCoverageString, StringComparison.OrdinalIgnoreCase))
                {
                    isCollectCodeCoverageEnabled = true;
                }

                builder.AppendSwitchIfNotNull("--collect:", arg);
            }
        }

        if (isCollectCodeCoverageEnabled || isRunSettingsEnabled)
        {
            // Pass TraceDataCollector path to vstest.console as TestAdapterPath if --collect "Code Coverage"
            // or --settings (User can enable code coverage from runsettings) option given.
            // Not parsing the runsettings for two reason:
            //    1. To keep no knowledge of runsettings structure in VSTestTask.
            //    2. Impact of adding adapter path always is minimal. (worst case: loads additional data collector assembly in datacollector process.)
            // This is required due to currently trace datacollector not ships with dotnet sdk, can be remove once we have
            // go code coverage x-plat.
            if (task.VSTestTraceDataCollectorDirectoryPath != null && !task.VSTestTraceDataCollectorDirectoryPath.ItemSpec.IsNullOrEmpty())
            {
                builder.AppendSwitchIfNotNull("--testAdapterPath:", task.VSTestTraceDataCollectorDirectoryPath);
            }
            else if (isCollectCodeCoverageEnabled)
            {
                // Not showing message in runsettings scenario, because we are not sure that code coverage is enabled.
                // User might be using older Microsoft.NET.Test.Sdk which don't have CodeCoverage infra.
                task.Log.LogWarning(Resources.Resources.UpdateTestSdkForCollectingCodeCoverage);
            }
        }

        if (task.VSTestNoLogo)
        {
            builder.AppendSwitch("--nologo");
            Environment.SetEnvironmentVariable("VSTEST_MSBUILD_NOLOGO", "1");
        }

        if (string.Equals(task.VSTestArtifactsProcessingMode, "collect", StringComparison.OrdinalIgnoreCase))
        {
            builder.AppendSwitch("--artifactsProcessingMode-collect");
        }

        builder.AppendSwitchIfNotNull("--testSessionCorrelationId:", task.VSTestSessionCorrelationId);

        // VSTestCLIRunSettings should be last argument as vstest.console ignore options after "--" (CLIRunSettings option).
        if (task.VSTestCLIRunSettings != null)
        {
            builder.AppendSwitch("--");
            foreach (var arg in task.VSTestCLIRunSettings)
            {
                builder.AppendSwitchIfNotNull(string.Empty, arg);
            }
        }

        return builder.ToString();
    }

    /// <summary>
    /// Resolves the full path to the dotnet host from the DOTNET_HOST_PATH environment variable.
    /// </summary>
    /// <returns>
    /// The resolved full path to the dotnet host, or <see langword="null"/> if the variable is not set or empty.
    /// </returns>
    internal static string? ResolveDotnetPath()
    {
        var dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
        return StringUtils.IsNullOrEmpty(dotnetHostPath) ? null : Path.GetFullPath(dotnetHostPath);
    }
}