From f182de1334a16621e7be2931c48e94c9e33c0066 Mon Sep 17 00:00:00 2001 From: Mathieu Cartoixa Date: Sat, 2 Jul 2022 22:45:04 +0200 Subject: [PATCH] Create a ToolTask based VSTestTask that integrates nicely in the MSBuild infrastructure (#680) This new task will be used by default during the build. If the $(VSTestUseConsole) property is set to True, the old console forwarding VSTestForwardTask will be used instead. --- .../Microsoft.TestPlatform.targets | 43 ++++ .../PublicAPI/PublicAPI.Unshipped.txt | 55 ++++ .../Tasks/ITestTask.cs | 39 +++ .../Tasks/TestTaskExtensions.cs | 236 +++++++++++++++++ .../Tasks/VSTestForwardingTask.cs | 237 +----------------- .../Tasks/VSTestTask.cs | 99 ++++++++ .../FakeBuildEngine.cs | 40 +++ .../VsTestTaskTests.cs | 55 ++-- 8 files changed, 542 insertions(+), 262 deletions(-) create mode 100644 src/Microsoft.TestPlatform.Build/Tasks/ITestTask.cs create mode 100644 src/Microsoft.TestPlatform.Build/Tasks/TestTaskExtensions.cs create mode 100644 src/Microsoft.TestPlatform.Build/Tasks/VSTestTask.cs create mode 100644 test/Microsoft.TestPlatform.Build.UnitTests/FakeBuildEngine.cs diff --git a/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets b/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets index a55c1e4aa6..88b479f61e 100644 --- a/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets +++ b/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets @@ -15,9 +15,11 @@ Copyright (c) .NET Foundation. All rights reserved. Microsoft.TestPlatform.Build.dll $([System.IO.Path]::Combine($(MSBuildThisFileDirectory),"vstest.console.dll")) False + False + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.TestPlatform.Build/PublicAPI/PublicAPI.Unshipped.txt b/src/Microsoft.TestPlatform.Build/PublicAPI/PublicAPI.Unshipped.txt index ba5b4e8453..71def80085 100644 --- a/src/Microsoft.TestPlatform.Build/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Microsoft.TestPlatform.Build/PublicAPI/PublicAPI.Unshipped.txt @@ -58,8 +58,63 @@ Microsoft.TestPlatform.Build.Tasks.VSTestForwardingTask.VSTestTraceDataCollector Microsoft.TestPlatform.Build.Tasks.VSTestForwardingTask.VSTestTraceDataCollectorDirectoryPath.set -> void Microsoft.TestPlatform.Build.Tasks.VSTestForwardingTask.VSTestVerbosity.get -> string? Microsoft.TestPlatform.Build.Tasks.VSTestForwardingTask.VSTestVerbosity.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask +Microsoft.TestPlatform.Build.Tasks.VSTestTask.TestFileFullPath.get -> Microsoft.Build.Framework.ITaskItem? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.TestFileFullPath.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlame.get -> bool +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlame.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameCrash.get -> bool +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameCrash.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameCrashCollectAlways.get -> bool +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameCrashCollectAlways.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameCrashDumpType.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameCrashDumpType.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameHang.get -> bool +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameHang.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameHangDumpType.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameHangDumpType.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameHangTimeout.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestBlameHangTimeout.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestCLIRunSettings.get -> string![]? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestCLIRunSettings.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestCollect.get -> string![]? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestCollect.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestConsolePath.get -> Microsoft.Build.Framework.ITaskItem? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestConsolePath.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestDiag.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestDiag.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestFramework.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestFramework.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestListTests.get -> bool +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestListTests.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestLogger.get -> string![]? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestLogger.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestNoLogo.get -> bool +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestNoLogo.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestArtifactsProcessingMode.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestArtifactsProcessingMode.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestSessionCorrelationId.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestSessionCorrelationId.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestPlatform.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestPlatform.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestResultsDirectory.get -> Microsoft.Build.Framework.ITaskItem? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestResultsDirectory.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestSetting.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestSetting.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestTask() -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestTestAdapterPath.get -> Microsoft.Build.Framework.ITaskItem![]? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestTestAdapterPath.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestTestCaseFilter.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestTestCaseFilter.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestTraceDataCollectorDirectoryPath.get -> Microsoft.Build.Framework.ITaskItem? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestTraceDataCollectorDirectoryPath.set -> void +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestVerbosity.get -> string? +Microsoft.TestPlatform.Build.Tasks.VSTestTask.VSTestVerbosity.set -> void Microsoft.TestPlatform.Build.Trace.Tracing override Microsoft.TestPlatform.Build.Tasks.VSTestLogsTask.Execute() -> bool override Microsoft.TestPlatform.Build.Tasks.VSTestForwardingTask.Execute() -> bool +override Microsoft.TestPlatform.Build.Tasks.VSTestTask.GenerateCommandLineCommands() -> string? +override Microsoft.TestPlatform.Build.Tasks.VSTestTask.GenerateFullPathToTool() -> string? +override Microsoft.TestPlatform.Build.Tasks.VSTestTask.ToolName.get -> string? static Microsoft.TestPlatform.Build.Trace.Tracing.Trace(string! message) -> void static Microsoft.TestPlatform.Build.Trace.Tracing.traceEnabled -> bool diff --git a/src/Microsoft.TestPlatform.Build/Tasks/ITestTask.cs b/src/Microsoft.TestPlatform.Build/Tasks/ITestTask.cs new file mode 100644 index 0000000000..e65c05703d --- /dev/null +++ b/src/Microsoft.TestPlatform.Build/Tasks/ITestTask.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.TestPlatform.Build.Tasks; + +internal interface ITestTask : ITask, ICancelableTask +{ + + ITaskItem? TestFileFullPath { get; set; } + string? VSTestSetting { get; set; } + ITaskItem[]? VSTestTestAdapterPath { get; set; } + string? VSTestFramework { get; set; } + string? VSTestPlatform { get; set; } + string? VSTestTestCaseFilter { get; set; } + string[]? VSTestLogger { get; set; } + bool VSTestListTests { get; set; } + string? VSTestDiag { get; set; } + string[]? VSTestCLIRunSettings { get; set; } + ITaskItem? VSTestConsolePath { get; set; } + ITaskItem? VSTestResultsDirectory { get; set; } + string? VSTestVerbosity { get; set; } + string[]? VSTestCollect { get; set; } + bool VSTestBlame { get; set; } + bool VSTestBlameCrash { get; set; } + string? VSTestBlameCrashDumpType { get; set; } + bool VSTestBlameCrashCollectAlways { get; set; } + bool VSTestBlameHang { get; set; } + string? VSTestBlameHangDumpType { get; set; } + string? VSTestBlameHangTimeout { get; set; } + ITaskItem? VSTestTraceDataCollectorDirectoryPath { get; set; } + bool VSTestNoLogo { get; set; } + string? VSTestArtifactsProcessingMode { get; set; } + string? VSTestSessionCorrelationId { get; set; } + + TaskLoggingHelper Log { get; } +} diff --git a/src/Microsoft.TestPlatform.Build/Tasks/TestTaskExtensions.cs b/src/Microsoft.TestPlatform.Build/Tasks/TestTaskExtensions.cs new file mode 100644 index 0000000000..e18d36ee57 --- /dev/null +++ b/src/Microsoft.TestPlatform.Build/Tasks/TestTaskExtensions.cs @@ -0,0 +1,236 @@ +// 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 Microsoft.Build.Utilities; + +namespace Microsoft.TestPlatform.Build.Tasks; + +internal static class TestTaskExtensions +{ + + public static string CreateCommandLineArguments(this ITestTask task) + { + const string codeCoverageString = "Code Coverage"; + const string vsTestAppName = "vstest.console.dll"; + + var isConsoleLoggerSpecifiedByUser = false; + var isCollectCodeCoverageEnabled = false; + var isRunSettingsEnabled = false; + + var builder = new CommandLineBuilder(); + builder.AppendSwitch("exec"); + if (task.VSTestConsolePath != null && !task.VSTestConsolePath.ItemSpec.IsNullOrEmpty()) + { + builder.AppendSwitchIfNotNull("", task.VSTestConsolePath); + } + else + { + builder.AppendSwitch(vsTestAppName); + } + + // TODO log arguments in task + if (!task.VSTestSetting.IsNullOrEmpty()) + { + isRunSettingsEnabled = true; + builder.AppendSwitchIfNotNull("--settings:", task.VSTestSetting); + } + + if (task.VSTestTestAdapterPath != null && task.VSTestTestAdapterPath.Any()) + { + foreach (var arg in task.VSTestTestAdapterPath) + { + builder.AppendSwitchIfNotNull("--testAdapterPath:", arg); + } + } + + if (!task.VSTestFramework.IsNullOrEmpty()) + { + 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); + } + + if (!task.VSTestTestCaseFilter.IsNullOrEmpty()) + { + builder.AppendSwitchIfNotNull("--testCaseFilter:", task.VSTestTestCaseFilter); + } + + if (task.VSTestLogger != null && task.VSTestLogger.Length > 0) + { + foreach (var arg in task.VSTestLogger) + { + builder.AppendSwitchIfNotNull("--logger:", arg); + + if (arg.StartsWith("console", StringComparison.OrdinalIgnoreCase)) + { + isConsoleLoggerSpecifiedByUser = true; + } + } + } + + if (task.VSTestResultsDirectory != null && !task.VSTestResultsDirectory.ItemSpec.IsNullOrEmpty()) + { + builder.AppendSwitchIfNotNull("--resultsDirectory:", task.VSTestResultsDirectory); + } + + if (task.VSTestListTests) + { + builder.AppendSwitch("--listTests"); + } + + if (!task.VSTestDiag.IsNullOrEmpty()) + { + builder.AppendSwitchIfNotNull("--Diag:", task.VSTestDiag); + } + + if (task.TestFileFullPath == null) + { + task.Log.LogError("Test file path cannot be empty or null."); + } + else + { + builder.AppendFileNameIfNotNull(task.TestFileFullPath); + } + + // Console logger was not specified by user, but verbosity was, hence add default console logger with verbosity as specified + if (!task.VSTestVerbosity.IsNullOrWhiteSpace() && !isConsoleLoggerSpecifiedByUser) + { + var normalTestLogging = new List() { "n", "normal", "d", "detailed", "diag", "diagnostic" }; + var quietTestLogging = new List() { "q", "quiet" }; + + string vsTestVerbosity = "minimal"; + if (normalTestLogging.Contains(task.VSTestVerbosity.ToLowerInvariant())) + { + vsTestVerbosity = "normal"; + } + else if (quietTestLogging.Contains(task.VSTestVerbosity.ToLowerInvariant())) + { + vsTestVerbosity = "quiet"; + } + + builder.AppendSwitchUnquotedIfNotNull("--logger:", $"Console;Verbosity={vsTestVerbosity}"); + } + + if (task.VSTestBlame || task.VSTestBlameCrash || task.VSTestBlameHang) + { + var dumpArgs = new List(); + 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.Any()) + { + builder.AppendSwitchIfNotNull("--Blame:", string.Join(";", dumpArgs)); + } + else + { + builder.AppendSwitch("--Blame"); + } + } + + if (task.VSTestCollect != null && task.VSTestCollect.Any()) + { + foreach (var arg in task.VSTestCollect) + { + // 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. + Console.WriteLine(Resources.Resources.UpdateTestSdkForCollectingCodeCoverage); + } + } + } + + if (task.VSTestNoLogo) + { + builder.AppendSwitch("--nologo"); + } + + if (!task.VSTestArtifactsProcessingMode.IsNullOrEmpty() && task.VSTestArtifactsProcessingMode.Equals("collect", StringComparison.OrdinalIgnoreCase)) + { + builder.AppendSwitch("--artifactsProcessingMode-collect"); + } + + if (!task.VSTestSessionCorrelationId.IsNullOrEmpty()) + { + builder.AppendSwitchIfNotNull("--testSessionCorrelationId:", task.VSTestSessionCorrelationId); + } + + // VSTestCLIRunSettings should be last argument as vstest.console ignore options after "--"(CLIRunSettings option). + if (task.VSTestCLIRunSettings != null && task.VSTestCLIRunSettings.Any()) + { + builder.AppendSwitch("--"); + foreach (var arg in task.VSTestCLIRunSettings) + { + builder.AppendSwitchIfNotNull(string.Empty, arg); + } + } + + return builder.ToString(); + } +} diff --git a/src/Microsoft.TestPlatform.Build/Tasks/VSTestForwardingTask.cs b/src/Microsoft.TestPlatform.Build/Tasks/VSTestForwardingTask.cs index cbe7e2fb96..bc0fe87f20 100644 --- a/src/Microsoft.TestPlatform.Build/Tasks/VSTestForwardingTask.cs +++ b/src/Microsoft.TestPlatform.Build/Tasks/VSTestForwardingTask.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading; using Microsoft.Build.Framework; @@ -13,13 +11,11 @@ namespace Microsoft.TestPlatform.Build.Tasks; -public class VSTestForwardingTask : Task, ICancelableTask +public class VSTestForwardingTask : Task, ITestTask { private int _activeProcessId; private const string DotnetExe = "dotnet"; - private const string VsTestAppName = "vstest.console.dll"; - private const string CodeCoverageString = "Code Coverage"; public ITaskItem? TestFileFullPath { get; set; } public string? VSTestSetting { get; set; } @@ -77,7 +73,7 @@ public override bool Execute() var processInfo = new ProcessStartInfo { FileName = DotnetExe, - Arguments = CreateArguments(), + Arguments = this.CreateCommandLineArguments(), UseShellExecute = false, }; @@ -110,233 +106,4 @@ public void Cancel() Tracing.Trace(string.Format("VSTest: Killing process throws ArgumentException with the following message {0}. It may be that process is not running", ex)); } } - - internal string CreateArguments() - { - var builder = new CommandLineBuilder(); - builder.AppendSwitch("exec"); - if (VSTestConsolePath != null && !VSTestConsolePath.ItemSpec.IsNullOrEmpty()) - { - builder.AppendSwitchIfNotNull("", VSTestConsolePath); - } - else - { - builder.AppendSwitch(VsTestAppName); - } - - CreateCommandLineArguments(builder); - - // VSTestCLIRunSettings should be last argument in allArgs as vstest.console ignore options after "--"(CLIRunSettings option). - AddCliRunSettingsArgs(builder); - - return builder.ToString(); - } - - private void AddCliRunSettingsArgs(CommandLineBuilder builder) - { - if (VSTestCLIRunSettings != null && VSTestCLIRunSettings.Any()) - { - builder.AppendSwitch("--"); - foreach (var arg in VSTestCLIRunSettings) - { - builder.AppendSwitchIfNotNull(string.Empty, arg); - } - } - } - - private void CreateCommandLineArguments(CommandLineBuilder builder) - { - var isConsoleLoggerSpecifiedByUser = false; - var isCollectCodeCoverageEnabled = false; - var isRunSettingsEnabled = false; - - // TODO log arguments in task - if (!VSTestSetting.IsNullOrEmpty()) - { - isRunSettingsEnabled = true; - builder.AppendSwitchIfNotNull("--settings:", VSTestSetting); - } - - if (VSTestTestAdapterPath != null && VSTestTestAdapterPath.Any()) - { - foreach (var arg in VSTestTestAdapterPath) - { - builder.AppendSwitchIfNotNull("--testAdapterPath:", arg); - } - } - - if (!VSTestFramework.IsNullOrEmpty()) - { - builder.AppendSwitchIfNotNull("--framework:", VSTestFramework); - } - - // vstest.console only support x86 and x64 for argument platform - if (!VSTestPlatform.IsNullOrEmpty() && !VSTestPlatform.Contains("AnyCPU")) - { - builder.AppendSwitchIfNotNull("--platform:", VSTestPlatform); - } - - if (!VSTestTestCaseFilter.IsNullOrEmpty()) - { - builder.AppendSwitchIfNotNull("--testCaseFilter:", VSTestTestCaseFilter); - } - - if (VSTestLogger != null && VSTestLogger.Length > 0) - { - foreach (var arg in VSTestLogger) - { - builder.AppendSwitchIfNotNull("--logger:", arg); - - if (arg.StartsWith("console", StringComparison.OrdinalIgnoreCase)) - { - isConsoleLoggerSpecifiedByUser = true; - } - } - } - - if (VSTestResultsDirectory != null && !VSTestResultsDirectory.ItemSpec.IsNullOrEmpty()) - { - builder.AppendSwitchIfNotNull("--resultsDirectory:", VSTestResultsDirectory); - } - - if (VSTestListTests) - { - builder.AppendSwitch("--listTests"); - } - - if (!VSTestDiag.IsNullOrEmpty()) - { - builder.AppendSwitchIfNotNull("--Diag:", VSTestDiag); - } - - if (TestFileFullPath == null) - { - Log.LogError("Test file path cannot be empty or null."); - } - else - { - builder.AppendFileNameIfNotNull(TestFileFullPath); - } - - // Console logger was not specified by user, but verbosity was, hence add default console logger with verbosity as specified - if (!VSTestVerbosity.IsNullOrWhiteSpace() && !isConsoleLoggerSpecifiedByUser) - { - var normalTestLogging = new List() { "n", "normal", "d", "detailed", "diag", "diagnostic" }; - var quietTestLogging = new List() { "q", "quiet" }; - - string vsTestVerbosity = "minimal"; - if (normalTestLogging.Contains(VSTestVerbosity.ToLowerInvariant())) - { - vsTestVerbosity = "normal"; - } - else if (quietTestLogging.Contains(VSTestVerbosity.ToLowerInvariant())) - { - vsTestVerbosity = "quiet"; - } - - builder.AppendSwitchUnquotedIfNotNull("--logger:", $"Console;Verbosity={vsTestVerbosity}"); - } - - if (VSTestBlame || VSTestBlameCrash || VSTestBlameHang) - { - var dumpArgs = new List(); - if (VSTestBlameCrash || VSTestBlameHang) - { - if (VSTestBlameCrash) - { - dumpArgs.Add("CollectDump"); - if (VSTestBlameCrashCollectAlways) - { - dumpArgs.Add($"CollectAlways={VSTestBlameCrashCollectAlways}"); - } - - if (!VSTestBlameCrashDumpType.IsNullOrEmpty()) - { - dumpArgs.Add($"DumpType={VSTestBlameCrashDumpType}"); - } - } - - if (VSTestBlameHang) - { - dumpArgs.Add("CollectHangDump"); - - if (!VSTestBlameHangDumpType.IsNullOrEmpty()) - { - dumpArgs.Add($"HangDumpType={VSTestBlameHangDumpType}"); - } - - if (!VSTestBlameHangTimeout.IsNullOrEmpty()) - { - dumpArgs.Add($"TestTimeout={VSTestBlameHangTimeout}"); - } - } - } - - if (dumpArgs.Any()) - { - builder.AppendSwitchIfNotNull("--Blame:", string.Join(";", dumpArgs)); - } - else - { - builder.AppendSwitch("--Blame"); - } - } - - if (VSTestCollect != null && VSTestCollect.Any()) - { - foreach (var arg in VSTestCollect) - { - // 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 (VSTestTraceDataCollectorDirectoryPath != null && !VSTestTraceDataCollectorDirectoryPath.ItemSpec.IsNullOrEmpty()) - { - builder.AppendSwitchIfNotNull("--testAdapterPath:", 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. - Console.WriteLine(Resources.Resources.UpdateTestSdkForCollectingCodeCoverage); - } - } - } - - if (VSTestNoLogo) - { - builder.AppendSwitch("--nologo"); - } - - if (!VSTestArtifactsProcessingMode.IsNullOrEmpty() && VSTestArtifactsProcessingMode.Equals("collect", StringComparison.OrdinalIgnoreCase)) - { - builder.AppendSwitch("--artifactsProcessingMode-collect"); - } - - if (!VSTestSessionCorrelationId.IsNullOrEmpty()) - { - builder.AppendSwitchIfNotNull("--testSessionCorrelationId:", VSTestSessionCorrelationId); - } - } } diff --git a/src/Microsoft.TestPlatform.Build/Tasks/VSTestTask.cs b/src/Microsoft.TestPlatform.Build/Tasks/VSTestTask.cs new file mode 100644 index 0000000000..f4258763bd --- /dev/null +++ b/src/Microsoft.TestPlatform.Build/Tasks/VSTestTask.cs @@ -0,0 +1,99 @@ +// 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.IO; +using System.Runtime.InteropServices; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.TestPlatform.Build.Tasks; + +public class VSTestTask : ToolTask, ITestTask +{ + + public ITaskItem? TestFileFullPath { get; set; } + public string? VSTestSetting { get; set; } + public ITaskItem[]? VSTestTestAdapterPath { get; set; } + public string? VSTestFramework { get; set; } + public string? VSTestPlatform { get; set; } + public string? VSTestTestCaseFilter { get; set; } + public string[]? VSTestLogger { get; set; } + public bool VSTestListTests { get; set; } + public string? VSTestDiag { get; set; } + public string[]? VSTestCLIRunSettings { get; set; } + [Required] + public ITaskItem? VSTestConsolePath { get; set; } + public ITaskItem? VSTestResultsDirectory { get; set; } + public string? VSTestVerbosity { get; set; } + public string[]? VSTestCollect { get; set; } + public bool VSTestBlame { get; set; } + public bool VSTestBlameCrash { get; set; } + public string? VSTestBlameCrashDumpType { get; set; } + public bool VSTestBlameCrashCollectAlways { get; set; } + public bool VSTestBlameHang { get; set; } + public string? VSTestBlameHangDumpType { get; set; } + public string? VSTestBlameHangTimeout { get; set; } + public ITaskItem? VSTestTraceDataCollectorDirectoryPath { get; set; } + public bool VSTestNoLogo { get; set; } + public string? VSTestArtifactsProcessingMode { get; set; } + public string? VSTestSessionCorrelationId { get; set; } + + protected override string? ToolName + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return "dotnet.exe"; + else + return "dotnet"; + } + } + + public VSTestTask() + { + LogStandardErrorAsError = true; + StandardOutputImportance = "Normal"; + } + + protected override string? GenerateCommandLineCommands() + { + return this.CreateCommandLineArguments(); + } + + protected override string? GenerateFullPathToTool() + { + if (!ToolPath.IsNullOrEmpty()) + { + return Path.Combine(Path.GetDirectoryName(Path.GetFullPath(ToolPath)), ToolExe); + } + + //TODO: https://github.com/dotnet/sdk/issues/20 Need to get the dotnet path from MSBuild? + + var dhp = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (!dhp.IsNullOrEmpty()) + { + var path = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(dhp)), ToolExe); + if (File.Exists(path)) + { + return path; + } + } + + if (File.Exists(ToolExe)) + { + return Path.GetFullPath(ToolExe); + } + + var values = Environment.GetEnvironmentVariable("PATH"); + foreach (var p in values.Split(Path.PathSeparator)) + { + var fullPath = Path.Combine(p, ToolExe); + if (File.Exists(fullPath)) + return fullPath; + } + + return null; + } +} diff --git a/test/Microsoft.TestPlatform.Build.UnitTests/FakeBuildEngine.cs b/test/Microsoft.TestPlatform.Build.UnitTests/FakeBuildEngine.cs new file mode 100644 index 0000000000..c0d077d74e --- /dev/null +++ b/test/Microsoft.TestPlatform.Build.UnitTests/FakeBuildEngine.cs @@ -0,0 +1,40 @@ +// 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.Collections; + +using Microsoft.Build.Framework; + +namespace Microsoft.TestPlatform.Build.UnitTests; + +public class FakeBuildEngine : IBuildEngine +{ + public bool ContinueOnError => false; + + public int LineNumberOfTaskNode => 0; + + public int ColumnNumberOfTaskNode => 0; + + public string ProjectFileOfTaskNode => string.Empty; + + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) + { + return false; + } + + public void LogCustomEvent(CustomBuildEventArgs e) + { + } + + public void LogErrorEvent(BuildErrorEventArgs e) + { + } + + public void LogMessageEvent(BuildMessageEventArgs e) + { + } + + public void LogWarningEvent(BuildWarningEventArgs e) + { + } +} diff --git a/test/Microsoft.TestPlatform.Build.UnitTests/VsTestTaskTests.cs b/test/Microsoft.TestPlatform.Build.UnitTests/VsTestTaskTests.cs index 27a5f852bb..de69294df3 100644 --- a/test/Microsoft.TestPlatform.Build.UnitTests/VsTestTaskTests.cs +++ b/test/Microsoft.TestPlatform.Build.UnitTests/VsTestTaskTests.cs @@ -13,12 +13,13 @@ namespace Microsoft.TestPlatform.Build.UnitTests; [TestClass] public class VsTestTaskTests { - private readonly VSTestForwardingTask _vsTestTask; + private readonly VSTestTask _vsTestTask; public VsTestTaskTests() { - _vsTestTask = new VSTestForwardingTask + _vsTestTask = new VSTestTask { + BuildEngine = new FakeBuildEngine(), TestFileFullPath = new TaskItem(@"C:\path\to\test-assembly.dll"), VSTestFramework = ".NETCoreapp,Version2.0" }; @@ -34,7 +35,7 @@ public void CreateArgumentShouldAddOneEntryForCLIRunSettings() _vsTestTask.VSTestCLIRunSettings[0] = arg1; _vsTestTask.VSTestCLIRunSettings[1] = arg2; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, " -- "); StringAssert.Contains(commandline, $"\"{arg1}\""); @@ -56,7 +57,7 @@ public void CreateArgumentShouldAddCLIRunSettingsArgAtEnd() _vsTestTask.VSTestCLIRunSettings[0] = arg1; _vsTestTask.VSTestCLIRunSettings[1] = arg2; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, " -- "); StringAssert.Contains(commandline, $"\"{arg1}\""); @@ -69,7 +70,7 @@ public void CreateArgumentShouldPassResultsDirectoryCorrectly() const string resultsDirectoryValue = @"C:\tmp\Results Directory"; _vsTestTask.VSTestResultsDirectory = new TaskItem(resultsDirectoryValue); - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, $"--resultsDirectory:\"{_vsTestTask.VSTestResultsDirectory?.ItemSpec}\""); } @@ -80,7 +81,7 @@ public void CreateArgumentShouldNotSetConsoleLoggerVerbosityIfConsoleLoggerIsGiv _vsTestTask.VSTestVerbosity = "diag"; _vsTestTask.VSTestLogger = new string[] { "Console;Verbosity=quiet" }; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.DoesNotMatch(commandline, new Regex("(--logger:\"Console;Verbosity=normal\")")); StringAssert.Contains(commandline, "--logger:\"Console;Verbosity=quiet\""); @@ -91,7 +92,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToNormalIfConsoleLogger { _vsTestTask.VSTestVerbosity = "n"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=normal"); } @@ -101,7 +102,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToNormalIfConsoleLogger { _vsTestTask.VSTestVerbosity = "normal"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=normal"); } @@ -111,7 +112,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToNormalIfConsoleLogger { _vsTestTask.VSTestVerbosity = "d"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=normal"); } @@ -121,7 +122,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToNormalIfConsoleLogger { _vsTestTask.VSTestVerbosity = "detailed"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=normal"); } @@ -131,7 +132,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToNormalIfConsoleLogger { _vsTestTask.VSTestVerbosity = "diag"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=normal"); } @@ -141,7 +142,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToNormalIfConsoleLogger { _vsTestTask.VSTestVerbosity = "diagnostic"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=normal"); } @@ -151,7 +152,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToQuietIfConsoleLoggerI { _vsTestTask.VSTestVerbosity = "q"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=quiet"); } @@ -161,7 +162,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToQuietIfConsoleLoggerI { _vsTestTask.VSTestVerbosity = "quiet"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=quiet"); } @@ -171,7 +172,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToMinimalIfConsoleLogge { _vsTestTask.VSTestVerbosity = "m"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=minimal"); } @@ -181,7 +182,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToMinimalIfConsoleLogge { _vsTestTask.VSTestVerbosity = "minimal"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=minimal"); } @@ -191,7 +192,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToNormalIfConsoleLogger { _vsTestTask.VSTestVerbosity = "Normal"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=normal"); } @@ -201,7 +202,7 @@ public void CreateArgumentShouldSetConsoleLoggerVerbosityToQuietIfConsoleLoggerI { _vsTestTask.VSTestVerbosity = "Quiet"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:Console;Verbosity=quiet"); } @@ -211,7 +212,7 @@ public void CreateArgumentShouldPreserveWhiteSpaceInLogger() { _vsTestTask.VSTestLogger = new string[] { "trx;LogFileName=foo bar.trx" }; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:\"trx;LogFileName=foo bar.trx\""); } @@ -224,7 +225,7 @@ public void CreateArgumentShouldAddOneCollectArgumentForEachCollect() _vsTestTask.VSTestCollect[0] = "name1"; _vsTestTask.VSTestCollect[1] = "name 2"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--collect:name1"); StringAssert.Contains(commandline, "--collect:\"name 2\""); @@ -235,7 +236,7 @@ public void CreateArgumentShouldAddMultipleTestAdapterPaths() { _vsTestTask.VSTestTestAdapterPath = new ITaskItem[] { new TaskItem("path1"), new TaskItem("path2") }; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--testAdapterPath:path1"); StringAssert.Contains(commandline, "--testAdapterPath:path2"); @@ -245,7 +246,7 @@ public void CreateArgumentShouldAddMultipleTestAdapterPaths() public void CreateArgumentShouldAddMultipleLoggers() { _vsTestTask.VSTestLogger = new string[] { "trx;LogFileName=foo bar.trx", "console" }; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--logger:\"trx;LogFileName=foo bar.trx\""); StringAssert.Contains(commandline, "--logger:console"); @@ -258,7 +259,7 @@ public void CreateArgumentShouldAddTraceCollectorDirectoryPathAsTestAdapterForCo _vsTestTask.VSTestTraceDataCollectorDirectoryPath = new TaskItem(traceDataCollectorDirectoryPath); _vsTestTask.VSTestCollect = new string[] { "code coverage" }; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); string expectedArg = $"--testAdapterPath:\"{_vsTestTask.VSTestTraceDataCollectorDirectoryPath?.ItemSpec}\""; StringAssert.Contains(commandline, expectedArg); @@ -271,7 +272,7 @@ public void CreateArgumentShouldNotAddTraceCollectorDirectoryPathAsTestAdapterFo _vsTestTask.VSTestTraceDataCollectorDirectoryPath = new TaskItem(traceDataCollectorDirectoryPath); _vsTestTask.VSTestCollect = new string[] { "not code coverage" }; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); string notExpectedArg = $"--testAdapterPath:\"{this._vsTestTask.VSTestTraceDataCollectorDirectoryPath?.ItemSpec}\""; StringAssert.DoesNotMatch(commandline, new Regex(Regex.Escape(notExpectedArg))); @@ -284,7 +285,7 @@ public void CreateArgumentShouldAddTraceCollectorDirectoryPathAsTestAdapterIfSet _vsTestTask.VSTestTraceDataCollectorDirectoryPath = new TaskItem(traceDataCollectorDirectoryPath); _vsTestTask.VSTestSetting = @"c:\path\to\sample.runsettings"; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); string expectedArg = $"--testAdapterPath:{_vsTestTask.VSTestTraceDataCollectorDirectoryPath?.ItemSpec}"; StringAssert.Contains(commandline, expectedArg); @@ -297,7 +298,7 @@ public void CreateArgumentShouldNotAddTestAdapterPathIfVSTestTraceDataCollectorD _vsTestTask.VSTestSetting = @"c:\path\to\sample.runsettings"; _vsTestTask.VSTestCollect = new string[] { "code coverage" }; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.DoesNotMatch(commandline, new Regex(@"(--testAdapterPath:)")); } @@ -307,7 +308,7 @@ public void CreateArgumentShouldAddNoLogoOptionIfSpecifiedByUser() { _vsTestTask.VSTestNoLogo = true; - var commandline = _vsTestTask.CreateArguments(); + var commandline = _vsTestTask.CreateCommandLineArguments(); StringAssert.Contains(commandline, "--nologo"); }