| File: Tasks\TestTaskUtils.cs | Web Access |
| Project: 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); } }