From 4fae80ce48a4c5a44a7e99c4a9e31a99f36c7c5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:36:18 +0000 Subject: [PATCH 1/4] Initial plan From c6fa7f359876f902d079ca9d9747c4886ef55790 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:49:34 +0000 Subject: [PATCH 2/4] Add --self-contained and --no-self-contained options to dotnet test command Co-authored-by: marcpopMSFT <12663534+marcpopMSFT@users.noreply.github.com> --- .../MicrosoftTestingPlatformTestCommand.cs | 4 + .../dotnet/Commands/Test/TestCommandParser.cs | 8 ++ .../Commands/Test/VSTest/TestCommand.cs | 4 + ...enDotnetTestBuildsAndRunsTestfromCsproj.cs | 81 +++++++++++++++++++ 4 files changed, 97 insertions(+) diff --git a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs index d84264e6c49e..ed960a88d6ef 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs @@ -40,6 +40,10 @@ private int RunInternal(ParseResult parseResult, bool isHelp) ValidationUtility.ValidateMutuallyExclusiveOptions(parseResult); ValidationUtility.ValidateSolutionOrProjectOrDirectoryOrModulesArePassedCorrectly(parseResult); + CommonOptions.ValidateSelfContainedOptions( + parseResult.HasOption(TestCommandParser.SelfContainedOption), + parseResult.HasOption(TestCommandParser.NoSelfContainedOption)); + int degreeOfParallelism = GetDegreeOfParallelism(parseResult); var testOptions = new TestOptions( IsHelp: isHelp, diff --git a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs index 57be7086f011..145ce40e00e9 100644 --- a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs +++ b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs @@ -158,6 +158,10 @@ private static Option CreateBlameHangDumpOption() public static readonly Option NoRestoreOption = CommonOptions.NoRestoreOption; + public static readonly Option SelfContainedOption = CommonOptions.SelfContainedOption; + + public static readonly Option NoSelfContainedOption = CommonOptions.NoSelfContainedOption; + public static readonly Option FrameworkOption = CommonOptions.FrameworkOption(CliCommandStrings.TestFrameworkOptionDescription); public static readonly Option ConfigurationOption = CommonOptions.ConfigurationOption(CliCommandStrings.TestConfigurationOptionDescription); @@ -250,6 +254,8 @@ private static Command GetTestingPlatformCliCommand() command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.TestRuntimeOptionDescription)); command.Options.Add(VerbosityOption); command.Options.Add(CommonOptions.NoRestoreOption); + command.Options.Add(SelfContainedOption); + command.Options.Add(NoSelfContainedOption); command.Options.Add(MicrosoftTestingPlatformOptions.NoBuildOption); command.Options.Add(MicrosoftTestingPlatformOptions.NoAnsiOption); command.Options.Add(MicrosoftTestingPlatformOptions.NoProgressOption); @@ -297,6 +303,8 @@ private static Command GetVSTestCliCommand() command.Options.Add(FrameworkOption); command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.TestRuntimeOptionDescription)); command.Options.Add(NoRestoreOption); + command.Options.Add(SelfContainedOption); + command.Options.Add(NoSelfContainedOption); command.Options.Add(CommonOptions.InteractiveMsBuildForwardOption); command.Options.Add(VerbosityOption); command.Options.Add(CommonOptions.ArchitectureOption); diff --git a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs index 5fc54e4e1630..77e434fb60fd 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs @@ -23,6 +23,10 @@ public static int Run(ParseResult parseResult) { parseResult.HandleDebugSwitch(); + CommonOptions.ValidateSelfContainedOptions( + parseResult.HasOption(TestCommandParser.SelfContainedOption), + parseResult.HasOption(TestCommandParser.NoSelfContainedOption)); + FeatureFlag.Instance.PrintFlagFeatureState(); // We use also current process id for the correlation id for possible future usage in case we need to know the parent process diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs index 6c054f61ceed..c91c2a033303 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs @@ -836,6 +836,87 @@ public void PropertiesEndingWithDotDllShouldNotFail(string property) result.ExitCode.Should().Be(1); } + [Fact] + public void ItTestsWithSelfContainedOption() + { + var testInstance = _testAssetsManager.CopyTestAsset("XunitCore") + .WithSource() + .WithVersionVariables(); + + var rootPath = testInstance.Path; + var rid = EnvironmentInfo.GetCompatibleRid(); + + var result = new DotnetTestCommand(Log, disableNewOutput: true, ConsoleLoggerOutputNormal) + .WithWorkingDirectory(rootPath) + .Execute("--runtime", rid, "--self-contained"); + + result + .Should() + .NotHaveStdErrContaining("NETSDK1179") + .And + .NotHaveStdErrContaining("MSB1001"); + + if (!TestContext.IsLocalized()) + { + result.StdOut.Should().Contain("Total tests: 2"); + result.StdOut.Should().Contain("Passed: 1"); + result.StdOut.Should().Contain("Failed: 1"); + } + } + + [Fact] + public void ItTestsWithNoSelfContainedOption() + { + var testInstance = _testAssetsManager.CopyTestAsset("XunitCore") + .WithSource() + .WithVersionVariables(); + + var rootPath = testInstance.Path; + var rid = EnvironmentInfo.GetCompatibleRid(); + + var result = new DotnetTestCommand(Log, disableNewOutput: true, ConsoleLoggerOutputNormal) + .WithWorkingDirectory(rootPath) + .Execute("--runtime", rid, "--no-self-contained"); + + result + .Should() + .NotHaveStdErrContaining("NETSDK1179") + .And + .NotHaveStdErrContaining("MSB1001"); + + if (!TestContext.IsLocalized()) + { + result.StdOut.Should().Contain("Total tests: 2"); + result.StdOut.Should().Contain("Passed: 1"); + result.StdOut.Should().Contain("Failed: 1"); + } + } + + [Fact] + public void ItFailsWhenBothSelfContainedAndNoSelfContainedAreSpecified() + { + var testInstance = _testAssetsManager.CopyTestAsset("XunitCore") + .WithSource() + .WithVersionVariables(); + + var rootPath = testInstance.Path; + var rid = EnvironmentInfo.GetCompatibleRid(); + + var result = new DotnetTestCommand(Log, disableNewOutput: true, ConsoleLoggerOutputNormal) + .WithWorkingDirectory(rootPath) + .Execute("--runtime", rid, "--self-contained", "--no-self-contained"); + + result + .Should() + .Fail(); + + if (!TestContext.IsLocalized()) + { + result.StdErr.Should().Contain("--self-contained") + .And.Contain("--no-self-contained"); + } + } + [Fact] public void DistributedLoggerEndingWithDotDllShouldBePassedToMSBuild() { From 07ef2e52fc280a37b4d73852e216ba0859d10eef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:16:16 +0000 Subject: [PATCH 3/4] Resolve merge conflict by adapting to refactored TestCommandDefinition - TestCommandParser was refactored in main branch with options moved to TestCommandDefinition - Added SelfContainedOption and NoSelfContainedOption to TestCommandDefinition - Added options to both ConfigureVSTestCommand and ConfigureTestingPlatformCommand - Updated all references from TestCommandParser to TestCommandDefinition throughout codebase - Fixed validation in TestCommand and MicrosoftTestingPlatformTestCommand to use TestCommandDefinition Co-authored-by: marcpopMSFT <12663534+marcpopMSFT@users.noreply.github.com> --- .../Commands/Test/MTP/MSBuildUtility.cs | 4 +- .../MicrosoftTestingPlatformTestCommand.cs | 4 +- .../Commands/Test/TestCommandDefinition.cs | 389 ++++++++++++++++++ .../dotnet/Commands/Test/TestCommandParser.cs | 365 +--------------- .../Commands/Test/VSTest/TestCommand.cs | 14 +- .../Extensions/OptionForwardingExtensions.cs | 2 +- src/Cli/dotnet/Telemetry/TelemetryFilter.cs | 4 +- .../Test/TestCommandParserTests.cs | 10 +- 8 files changed, 421 insertions(+), 371 deletions(-) create mode 100644 src/Cli/dotnet/Commands/Test/TestCommandDefinition.cs diff --git a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs index 839aa7b9b5a3..06e78ce4718a 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs @@ -104,7 +104,7 @@ public static BuildOptions GetBuildOptions(ParseResult parseResult) pathOptions, parseResult.GetValue(CommonOptions.NoRestoreOption), parseResult.GetValue(MicrosoftTestingPlatformOptions.NoBuildOption), - parseResult.HasOption(TestCommandParser.VerbosityOption) ? parseResult.GetValue(TestCommandParser.VerbosityOption) : null, + parseResult.HasOption(TestCommandDefinition.VerbosityOption) ? parseResult.GetValue(TestCommandDefinition.VerbosityOption) : null, parseResult.GetValue(MicrosoftTestingPlatformOptions.NoLaunchProfileOption), parseResult.GetValue(MicrosoftTestingPlatformOptions.NoLaunchProfileArgumentsOption), otherArgs, @@ -124,7 +124,7 @@ private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOption msbuildArgs.Add($"-verbosity:quiet"); } - var parsedMSBuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(msbuildArgs, CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, TestCommandParser.MTPTargetOption, TestCommandParser.VerbosityOption, CommonOptions.NoLogoOption()); + var parsedMSBuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(msbuildArgs, CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, TestCommandDefinition.MTPTargetOption, TestCommandDefinition.VerbosityOption, CommonOptions.NoLogoOption()); int result = new RestoringCommand(parsedMSBuildArgs, buildOptions.HasNoRestore).Execute(); diff --git a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs index ed960a88d6ef..44e739d91910 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs @@ -41,8 +41,8 @@ private int RunInternal(ParseResult parseResult, bool isHelp) ValidationUtility.ValidateSolutionOrProjectOrDirectoryOrModulesArePassedCorrectly(parseResult); CommonOptions.ValidateSelfContainedOptions( - parseResult.HasOption(TestCommandParser.SelfContainedOption), - parseResult.HasOption(TestCommandParser.NoSelfContainedOption)); + parseResult.HasOption(TestCommandDefinition.SelfContainedOption), + parseResult.HasOption(TestCommandDefinition.NoSelfContainedOption)); int degreeOfParallelism = GetDegreeOfParallelism(parseResult); var testOptions = new TestOptions( diff --git a/src/Cli/dotnet/Commands/Test/TestCommandDefinition.cs b/src/Cli/dotnet/Commands/Test/TestCommandDefinition.cs new file mode 100644 index 000000000000..a0de338d2703 --- /dev/null +++ b/src/Cli/dotnet/Commands/Test/TestCommandDefinition.cs @@ -0,0 +1,389 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.DotNet.Cli.Extensions; +using Command = System.CommandLine.Command; + +namespace Microsoft.DotNet.Cli.Commands.Test; + +internal static class TestCommandDefinition +{ + public enum TestRunner + { + VSTest, + MicrosoftTestingPlatform + } + + private sealed class GlobalJsonModel + { + [JsonPropertyName("test")] + public GlobalJsonTestNode Test { get; set; } = null!; + } + + private sealed class GlobalJsonTestNode + { + [JsonPropertyName("runner")] + public string RunnerName { get; set; } = null!; + } + + public const string Name = "test"; + public static readonly string DocsLink = "https://aka.ms/dotnet-test"; + + public static readonly Option SettingsOption = new Option("--settings", "-s") + { + Description = CliCommandStrings.CmdSettingsDescription, + HelpName = CliCommandStrings.CmdSettingsFile + }.ForwardAsSingle(o => $"-property:VSTestSetting={SurroundWithDoubleQuotes(CommandDirectoryContext.GetFullPath(o))}"); + + public static readonly Option ListTestsOption = new Option("--list-tests", "-t") + { + Description = CliCommandStrings.CmdListTestsDescription, + Arity = ArgumentArity.Zero + }.ForwardAs("-property:VSTestListTests=true"); + + public static readonly Option FilterOption = new Option("--filter") + { + Description = CliCommandStrings.CmdTestCaseFilterDescription, + HelpName = CliCommandStrings.CmdTestCaseFilterExpression + }.ForwardAsSingle(o => $"-property:VSTestTestCaseFilter={SurroundWithDoubleQuotes(o!)}"); + + public static readonly Option> AdapterOption = new Option>("--test-adapter-path") + { + Description = CliCommandStrings.CmdTestAdapterPathDescription, + HelpName = CliCommandStrings.CmdTestAdapterPath + }.ForwardAsSingle(o => $"-property:VSTestTestAdapterPath={SurroundWithDoubleQuotes(string.Join(";", o!.Select(CommandDirectoryContext.GetFullPath)))}") + .AllowSingleArgPerToken(); + + public static readonly Option> LoggerOption = new Option>("--logger", "-l") + { + Description = CliCommandStrings.CmdLoggerDescription, + HelpName = CliCommandStrings.CmdLoggerOption + }.ForwardAsSingle(o => + { + var loggersString = string.Join(";", GetSemiColonEscapedArgs(o!)); + + return $"-property:VSTestLogger={SurroundWithDoubleQuotes(loggersString)}"; + }) + .AllowSingleArgPerToken(); + + public static readonly Option OutputOption = new Option("--output", "-o") + { + Description = CliCommandStrings.CmdOutputDescription, + HelpName = CliCommandStrings.TestCmdOutputDir + } + .ForwardAsOutputPath("OutputPath", true); + + public static readonly Option DiagOption = new Option("--diag", "-d") + { + Description = CliCommandStrings.CmdPathTologFileDescription, + HelpName = CliCommandStrings.CmdPathToLogFile + } + .ForwardAsSingle(o => $"-property:VSTestDiag={SurroundWithDoubleQuotes(CommandDirectoryContext.GetFullPath(o))}"); + + public static readonly Option NoBuildOption = new Option("--no-build") + { + Description = CliCommandStrings.CmdNoBuildDescription, + Arity = ArgumentArity.Zero + }.ForwardAs("-property:VSTestNoBuild=true"); + + public static readonly Option ResultsOption = new Option("--results-directory") + { + Description = CliCommandStrings.CmdResultsDirectoryDescription, + HelpName = CliCommandStrings.CmdPathToResultsDirectory + }.ForwardAsSingle(o => $"-property:VSTestResultsDirectory={SurroundWithDoubleQuotes(CommandDirectoryContext.GetFullPath(o))}"); + + public static readonly Option> CollectOption = new Option>("--collect") + { + Description = CliCommandStrings.cmdCollectDescription, + HelpName = CliCommandStrings.cmdCollectFriendlyName + }.ForwardAsSingle(o => $"-property:VSTestCollect=\"{string.Join(";", GetSemiColonEscapedArgs(o!))}\"") + .AllowSingleArgPerToken(); + + public static readonly Option BlameOption = new Option("--blame") + { + Description = CliCommandStrings.CmdBlameDescription, + Arity = ArgumentArity.Zero + }.ForwardIfEnabled("-property:VSTestBlame=true"); + + public static readonly Option BlameCrashOption = new Option("--blame-crash") + { + Description = CliCommandStrings.CmdBlameCrashDescription, + Arity = ArgumentArity.Zero + }.ForwardIfEnabled("-property:VSTestBlameCrash=true"); + + public static readonly Option BlameCrashDumpOption = CreateBlameCrashDumpOption(); + + private static Option CreateBlameCrashDumpOption() + { + Option result = new Option("--blame-crash-dump-type") + { + Description = CliCommandStrings.CmdBlameCrashDumpTypeDescription, + HelpName = CliCommandStrings.CrashDumpTypeArgumentName, + } + .ForwardAsMany(o => ["-property:VSTestBlameCrash=true", $"-property:VSTestBlameCrashDumpType={o}"]); + result.AcceptOnlyFromAmong(["full", "mini"]); + return result; + } + + public static readonly Option BlameCrashAlwaysOption = new Option("--blame-crash-collect-always") + { + Description = CliCommandStrings.CmdBlameCrashCollectAlwaysDescription, + Arity = ArgumentArity.Zero + }.ForwardIfEnabled(["-property:VSTestBlameCrash=true", "-property:VSTestBlameCrashCollectAlways=true"]); + + public static readonly Option BlameHangOption = new Option("--blame-hang") + { + Description = CliCommandStrings.CmdBlameHangDescription, + Arity = ArgumentArity.Zero + }.ForwardAs("-property:VSTestBlameHang=true"); + + public static readonly Option BlameHangDumpOption = CreateBlameHangDumpOption(); + + private static Option CreateBlameHangDumpOption() + { + Option result = new Option("--blame-hang-dump-type") + { + Description = CliCommandStrings.CmdBlameHangDumpTypeDescription, + HelpName = CliCommandStrings.HangDumpTypeArgumentName + } + .ForwardAsMany(o => ["-property:VSTestBlameHang=true", $"-property:VSTestBlameHangDumpType={o}"]); + result.AcceptOnlyFromAmong(["full", "mini", "none"]); + return result; + } + + public static readonly Option BlameHangTimeoutOption = new Option("--blame-hang-timeout") + { + Description = CliCommandStrings.CmdBlameHangTimeoutDescription, + HelpName = CliCommandStrings.HangTimeoutArgumentName + }.ForwardAsMany(o => ["-property:VSTestBlameHang=true", $"-property:VSTestBlameHangTimeout={o}"]); + + public static readonly Option NoLogoOption = CommonOptions.NoLogoOption(forwardAs: "--property:VSTestNoLogo=true", description: CliCommandStrings.TestCmdNoLogo); + + public static readonly Option NoRestoreOption = CommonOptions.NoRestoreOption; + + public static readonly Option SelfContainedOption = CommonOptions.SelfContainedOption; + + public static readonly Option NoSelfContainedOption = CommonOptions.NoSelfContainedOption; + + public static readonly Option FrameworkOption = CommonOptions.FrameworkOption(CliCommandStrings.TestFrameworkOptionDescription); + + public static readonly Option ConfigurationOption = CommonOptions.ConfigurationOption(CliCommandStrings.TestConfigurationOptionDescription); + + public static readonly Option VerbosityOption = CommonOptions.VerbosityOption(); + public static readonly Option VsTestTargetOption = CommonOptions.RequiredMSBuildTargetOption("VSTest"); + public static readonly Option MTPTargetOption = CommonOptions.RequiredMSBuildTargetOption(CliConstants.MTPTarget); + + public static TestRunner GetTestRunner() + { + string? globalJsonPath = GetGlobalJsonPath(Environment.CurrentDirectory); + if (!File.Exists(globalJsonPath)) + { + return TestRunner.VSTest; + } + + string jsonText = File.ReadAllText(globalJsonPath); + + // This code path is hit exactly once during the whole life of the dotnet process. + // So, no concern about caching JsonSerializerOptions. + var globalJson = JsonSerializer.Deserialize(jsonText, new JsonSerializerOptions() + { + AllowDuplicateProperties = false, + AllowTrailingCommas = false, + ReadCommentHandling = JsonCommentHandling.Skip, + }); + + var name = globalJson?.Test?.RunnerName; + + if (name is null || name.Equals(CliConstants.VSTest, StringComparison.OrdinalIgnoreCase)) + { + return TestRunner.VSTest; + } + + if (name.Equals(CliConstants.MicrosoftTestingPlatform, StringComparison.OrdinalIgnoreCase)) + { + return TestRunner.MicrosoftTestingPlatform; + } + + throw new InvalidOperationException(string.Format(CliCommandStrings.CmdUnsupportedTestRunnerDescription, name)); + } + + private static string? GetGlobalJsonPath(string? startDir) + { + string? directory = startDir; + while (directory != null) + { + string globalJsonPath = Path.Combine(directory, "global.json"); + if (File.Exists(globalJsonPath)) + { + return globalJsonPath; + } + + directory = Path.GetDirectoryName(directory); + } + return null; + } + + public static Command Create() + { + var command = new Command(Name); + + switch (GetTestRunner()) + { + case TestRunner.VSTest: + ConfigureVSTestCommand(command); + break; + + case TestRunner.MicrosoftTestingPlatform: + ConfigureTestingPlatformCommand(command); + break; + + default: + throw new InvalidOperationException(); + }; + + return command; + } + + public static void ConfigureTestingPlatformCommand(Command command) + { + command.Description = CliCommandStrings.DotnetTestCommandMTPDescription; + command.Options.Add(MicrosoftTestingPlatformOptions.ProjectOrSolutionOption); + command.Options.Add(MicrosoftTestingPlatformOptions.SolutionOption); + command.Options.Add(MicrosoftTestingPlatformOptions.TestModulesFilterOption); + command.Options.Add(MicrosoftTestingPlatformOptions.TestModulesRootDirectoryOption); + command.Options.Add(MicrosoftTestingPlatformOptions.ResultsDirectoryOption); + command.Options.Add(MicrosoftTestingPlatformOptions.ConfigFileOption); + command.Options.Add(MicrosoftTestingPlatformOptions.DiagnosticOutputDirectoryOption); + command.Options.Add(MicrosoftTestingPlatformOptions.MaxParallelTestModulesOption); + command.Options.Add(MicrosoftTestingPlatformOptions.MinimumExpectedTestsOption); + command.Options.Add(CommonOptions.ArchitectureOption); + command.Options.Add(CommonOptions.EnvOption); + command.Options.Add(CommonOptions.PropertiesOption); + command.Options.Add(MicrosoftTestingPlatformOptions.ConfigurationOption); + command.Options.Add(MicrosoftTestingPlatformOptions.FrameworkOption); + command.Options.Add(CommonOptions.OperatingSystemOption); + command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.TestRuntimeOptionDescription)); + command.Options.Add(VerbosityOption); + command.Options.Add(CommonOptions.NoRestoreOption); + command.Options.Add(SelfContainedOption); + command.Options.Add(NoSelfContainedOption); + command.Options.Add(MicrosoftTestingPlatformOptions.NoBuildOption); + command.Options.Add(MicrosoftTestingPlatformOptions.NoAnsiOption); + command.Options.Add(MicrosoftTestingPlatformOptions.NoProgressOption); + command.Options.Add(MicrosoftTestingPlatformOptions.OutputOption); + command.Options.Add(MicrosoftTestingPlatformOptions.ListTestsOption); + command.Options.Add(MicrosoftTestingPlatformOptions.NoLaunchProfileOption); + command.Options.Add(MicrosoftTestingPlatformOptions.NoLaunchProfileArgumentsOption); + command.Options.Add(MTPTargetOption); + } + + public static void ConfigureVSTestCommand(Command command) + { + command.Description = CliCommandStrings.DotnetTestCommandVSTestDescription; + command.TreatUnmatchedTokensAsErrors = false; + command.DocsLink = DocsLink; + + // We are on purpose not capturing the solution, project or directory here. We want to pass it to the + // MSBuild command so we are letting it flow. + + command.Options.Add(SettingsOption); + command.Options.Add(ListTestsOption); + command.Options.Add(CommonOptions.TestEnvOption); + command.Options.Add(FilterOption); + command.Options.Add(AdapterOption); + command.Options.Add(LoggerOption); + command.Options.Add(OutputOption); + command.Options.Add(CommonOptions.ArtifactsPathOption); + command.Options.Add(DiagOption); + command.Options.Add(NoBuildOption); + command.Options.Add(ResultsOption); + command.Options.Add(CollectOption); + command.Options.Add(BlameOption); + command.Options.Add(BlameCrashOption); + command.Options.Add(BlameCrashDumpOption); + command.Options.Add(BlameCrashAlwaysOption); + command.Options.Add(BlameHangOption); + command.Options.Add(BlameHangDumpOption); + command.Options.Add(BlameHangTimeoutOption); + command.Options.Add(NoLogoOption); + command.Options.Add(ConfigurationOption); + command.Options.Add(FrameworkOption); + command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.TestRuntimeOptionDescription)); + command.Options.Add(NoRestoreOption); + command.Options.Add(SelfContainedOption); + command.Options.Add(NoSelfContainedOption); + command.Options.Add(CommonOptions.InteractiveMsBuildForwardOption); + command.Options.Add(VerbosityOption); + command.Options.Add(CommonOptions.ArchitectureOption); + command.Options.Add(CommonOptions.OperatingSystemOption); + command.Options.Add(CommonOptions.PropertiesOption); + command.Options.Add(CommonOptions.DisableBuildServersOption); + command.Options.Add(VsTestTargetOption); + } + + private static string GetSemiColonEscapedstring(string arg) + { + if (arg.IndexOf(";") != -1) + { + return arg.Replace(";", "%3b"); + } + + return arg; + } + + private static string[] GetSemiColonEscapedArgs(IEnumerable args) + { + int counter = 0; + string[] array = new string[args.Count()]; + + foreach (string arg in args) + { + array[counter++] = GetSemiColonEscapedstring(arg); + } + + return array; + } + + /// + /// Adding double quotes around the property helps MSBuild arguments parser and avoid incorrect splits on ',' or ';'. + /// + internal /* for testing purposes */ static string SurroundWithDoubleQuotes(string input) + { + if (input is null) + { + throw new ArgumentNullException(nameof(input)); + } + + // If already escaped by double quotes then return original string. + if (input.StartsWith("\"", StringComparison.Ordinal) + && input.EndsWith("\"", StringComparison.Ordinal)) + { + return input; + } + + // We want to count the number of trailing backslashes to ensure + // we will have an even number before adding the final double quote. + // Otherwise the last \" will be interpreted as escaping the double + // quote rather than a backslash and a double quote. + var trailingBackslashesCount = 0; + for (int i = input.Length - 1; i >= 0; i--) + { + if (input[i] == '\\') + { + trailingBackslashesCount++; + } + else + { + break; + } + } + + return trailingBackslashesCount % 2 == 0 + ? string.Concat("\"", input, "\"") + : string.Concat("\"", input, "\\\""); + } +} diff --git a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs index 145ce40e00e9..2ee6a66fc750 100644 --- a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs +++ b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs @@ -1,381 +1,42 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.CommandLine; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.DotNet.Cli.CommandLine; -using Microsoft.DotNet.Cli.Extensions; using Command = System.CommandLine.Command; namespace Microsoft.DotNet.Cli.Commands.Test; internal static class TestCommandParser { - private sealed class GlobalJsonModel - { - [JsonPropertyName("test")] - public GlobalJsonTestNode Test { get; set; } = null!; - } - - private sealed class GlobalJsonTestNode - { - [JsonPropertyName("runner")] - public string RunnerName { get; set; } = null!; - } - - public static readonly string DocsLink = "https://aka.ms/dotnet-test"; - - public static readonly Option SettingsOption = new Option("--settings", "-s") - { - Description = CliCommandStrings.CmdSettingsDescription, - HelpName = CliCommandStrings.CmdSettingsFile - }.ForwardAsSingle(o => $"-property:VSTestSetting={SurroundWithDoubleQuotes(CommandDirectoryContext.GetFullPath(o))}"); - - public static readonly Option ListTestsOption = new Option("--list-tests", "-t") - { - Description = CliCommandStrings.CmdListTestsDescription, - Arity = ArgumentArity.Zero - }.ForwardAs("-property:VSTestListTests=true"); - - public static readonly Option FilterOption = new Option("--filter") - { - Description = CliCommandStrings.CmdTestCaseFilterDescription, - HelpName = CliCommandStrings.CmdTestCaseFilterExpression - }.ForwardAsSingle(o => $"-property:VSTestTestCaseFilter={SurroundWithDoubleQuotes(o!)}"); - - public static readonly Option> AdapterOption = new Option>("--test-adapter-path") - { - Description = CliCommandStrings.CmdTestAdapterPathDescription, - HelpName = CliCommandStrings.CmdTestAdapterPath - }.ForwardAsSingle(o => $"-property:VSTestTestAdapterPath={SurroundWithDoubleQuotes(string.Join(";", o!.Select(CommandDirectoryContext.GetFullPath)))}") - .AllowSingleArgPerToken(); - - public static readonly Option> LoggerOption = new Option>("--logger", "-l") - { - Description = CliCommandStrings.CmdLoggerDescription, - HelpName = CliCommandStrings.CmdLoggerOption - }.ForwardAsSingle(o => - { - var loggersString = string.Join(";", GetSemiColonEscapedArgs(o!)); - - return $"-property:VSTestLogger={SurroundWithDoubleQuotes(loggersString)}"; - }) - .AllowSingleArgPerToken(); - - public static readonly Option OutputOption = new Option("--output", "-o") - { - Description = CliCommandStrings.CmdOutputDescription, - HelpName = CliCommandStrings.TestCmdOutputDir - } - .ForwardAsOutputPath("OutputPath", true); - - public static readonly Option DiagOption = new Option("--diag", "-d") - { - Description = CliCommandStrings.CmdPathTologFileDescription, - HelpName = CliCommandStrings.CmdPathToLogFile - } - .ForwardAsSingle(o => $"-property:VSTestDiag={SurroundWithDoubleQuotes(CommandDirectoryContext.GetFullPath(o))}"); - - public static readonly Option NoBuildOption = new Option("--no-build") - { - Description = CliCommandStrings.CmdNoBuildDescription, - Arity = ArgumentArity.Zero - }.ForwardAs("-property:VSTestNoBuild=true"); - - public static readonly Option ResultsOption = new Option("--results-directory") - { - Description = CliCommandStrings.CmdResultsDirectoryDescription, - HelpName = CliCommandStrings.CmdPathToResultsDirectory - }.ForwardAsSingle(o => $"-property:VSTestResultsDirectory={SurroundWithDoubleQuotes(CommandDirectoryContext.GetFullPath(o))}"); - - public static readonly Option> CollectOption = new Option>("--collect") - { - Description = CliCommandStrings.cmdCollectDescription, - HelpName = CliCommandStrings.cmdCollectFriendlyName - }.ForwardAsSingle(o => $"-property:VSTestCollect=\"{string.Join(";", GetSemiColonEscapedArgs(o!))}\"") - .AllowSingleArgPerToken(); - - public static readonly Option BlameOption = new Option("--blame") - { - Description = CliCommandStrings.CmdBlameDescription, - Arity = ArgumentArity.Zero - }.ForwardIfEnabled("-property:VSTestBlame=true"); - - public static readonly Option BlameCrashOption = new Option("--blame-crash") - { - Description = CliCommandStrings.CmdBlameCrashDescription, - Arity = ArgumentArity.Zero - }.ForwardIfEnabled("-property:VSTestBlameCrash=true"); - - public static readonly Option BlameCrashDumpOption = CreateBlameCrashDumpOption(); - - private static Option CreateBlameCrashDumpOption() - { - Option result = new Option("--blame-crash-dump-type") - { - Description = CliCommandStrings.CmdBlameCrashDumpTypeDescription, - HelpName = CliCommandStrings.CrashDumpTypeArgumentName, - } - .ForwardAsMany(o => ["-property:VSTestBlameCrash=true", $"-property:VSTestBlameCrashDumpType={o}"]); - result.AcceptOnlyFromAmong(["full", "mini"]); - return result; - } - - public static readonly Option BlameCrashAlwaysOption = new Option("--blame-crash-collect-always") - { - Description = CliCommandStrings.CmdBlameCrashCollectAlwaysDescription, - Arity = ArgumentArity.Zero - }.ForwardIfEnabled(["-property:VSTestBlameCrash=true", "-property:VSTestBlameCrashCollectAlways=true"]); - - public static readonly Option BlameHangOption = new Option("--blame-hang") - { - Description = CliCommandStrings.CmdBlameHangDescription, - Arity = ArgumentArity.Zero - }.ForwardAs("-property:VSTestBlameHang=true"); - - public static readonly Option BlameHangDumpOption = CreateBlameHangDumpOption(); - - private static Option CreateBlameHangDumpOption() - { - Option result = new Option("--blame-hang-dump-type") - { - Description = CliCommandStrings.CmdBlameHangDumpTypeDescription, - HelpName = CliCommandStrings.HangDumpTypeArgumentName - } - .ForwardAsMany(o => ["-property:VSTestBlameHang=true", $"-property:VSTestBlameHangDumpType={o}"]); - result.AcceptOnlyFromAmong(["full", "mini", "none"]); - return result; - } - - public static readonly Option BlameHangTimeoutOption = new Option("--blame-hang-timeout") - { - Description = CliCommandStrings.CmdBlameHangTimeoutDescription, - HelpName = CliCommandStrings.HangTimeoutArgumentName - }.ForwardAsMany(o => ["-property:VSTestBlameHang=true", $"-property:VSTestBlameHangTimeout={o}"]); - - public static readonly Option NoLogoOption = CommonOptions.NoLogoOption(forwardAs: "--property:VSTestNoLogo=true", description: CliCommandStrings.TestCmdNoLogo); - - public static readonly Option NoRestoreOption = CommonOptions.NoRestoreOption; - - public static readonly Option SelfContainedOption = CommonOptions.SelfContainedOption; - - public static readonly Option NoSelfContainedOption = CommonOptions.NoSelfContainedOption; - - public static readonly Option FrameworkOption = CommonOptions.FrameworkOption(CliCommandStrings.TestFrameworkOptionDescription); - - public static readonly Option ConfigurationOption = CommonOptions.ConfigurationOption(CliCommandStrings.TestConfigurationOptionDescription); - - public static readonly Option VerbosityOption = CommonOptions.VerbosityOption(); - public static readonly Option VsTestTargetOption = CommonOptions.RequiredMSBuildTargetOption("VSTest"); - public static readonly Option MTPTargetOption = CommonOptions.RequiredMSBuildTargetOption(CliConstants.MTPTarget); - - - private static readonly Command Command = ConstructCommand(); + private static readonly Command Command = CreateCommand(); public static Command GetCommand() { return Command; } - private static string GetTestRunnerName() - { - string? globalJsonPath = GetGlobalJsonPath(Environment.CurrentDirectory); - if (!File.Exists(globalJsonPath)) - { - return CliConstants.VSTest; - } - - string jsonText = File.ReadAllText(globalJsonPath); - - // This code path is hit exactly once during the whole life of the dotnet process. - // So, no concern about caching JsonSerializerOptions. - var globalJson = JsonSerializer.Deserialize(jsonText, new JsonSerializerOptions() - { - AllowDuplicateProperties = false, - AllowTrailingCommas = false, - ReadCommentHandling = JsonCommentHandling.Skip, - }); - - return globalJson?.Test?.RunnerName ?? CliConstants.VSTest; - } - - private static string? GetGlobalJsonPath(string? startDir) + private static Command CreateCommand() { - string? directory = startDir; - while (directory != null) + return TestCommandDefinition.GetTestRunner() switch { - string globalJsonPath = Path.Combine(directory, "global.json"); - if (File.Exists(globalJsonPath)) - { - return globalJsonPath; - } - - directory = Path.GetDirectoryName(directory); - } - return null; - } - - private static Command ConstructCommand() - { - string testRunnerName = GetTestRunnerName(); - - if (testRunnerName.Equals(CliConstants.VSTest, StringComparison.OrdinalIgnoreCase)) - { - return GetVSTestCliCommand(); - } - else if (testRunnerName.Equals(CliConstants.MicrosoftTestingPlatform, StringComparison.OrdinalIgnoreCase)) - { - return GetTestingPlatformCliCommand(); - } - - throw new InvalidOperationException(string.Format(CliCommandStrings.CmdUnsupportedTestRunnerDescription, testRunnerName)); + TestCommandDefinition.TestRunner.VSTest => CreateVSTestCommand(), + TestCommandDefinition.TestRunner.MicrosoftTestingPlatform => CreateTestingPlatformCommand(), + _ => throw new NotSupportedException(), + }; } - private static Command GetTestingPlatformCliCommand() + private static Command CreateTestingPlatformCommand() { - var command = new MicrosoftTestingPlatformTestCommand("test", CliCommandStrings.DotnetTestCommandMTPDescription); + var command = new MicrosoftTestingPlatformTestCommand(TestCommandDefinition.Name); + TestCommandDefinition.ConfigureTestingPlatformCommand(command); command.SetAction(parseResult => command.Run(parseResult)); - command.Options.Add(MicrosoftTestingPlatformOptions.ProjectOrSolutionOption); - command.Options.Add(MicrosoftTestingPlatformOptions.SolutionOption); - command.Options.Add(MicrosoftTestingPlatformOptions.TestModulesFilterOption); - command.Options.Add(MicrosoftTestingPlatformOptions.TestModulesRootDirectoryOption); - command.Options.Add(MicrosoftTestingPlatformOptions.ResultsDirectoryOption); - command.Options.Add(MicrosoftTestingPlatformOptions.ConfigFileOption); - command.Options.Add(MicrosoftTestingPlatformOptions.DiagnosticOutputDirectoryOption); - command.Options.Add(MicrosoftTestingPlatformOptions.MaxParallelTestModulesOption); - command.Options.Add(MicrosoftTestingPlatformOptions.MinimumExpectedTestsOption); - command.Options.Add(CommonOptions.ArchitectureOption); - command.Options.Add(CommonOptions.EnvOption); - command.Options.Add(CommonOptions.PropertiesOption); - command.Options.Add(MicrosoftTestingPlatformOptions.ConfigurationOption); - command.Options.Add(MicrosoftTestingPlatformOptions.FrameworkOption); - command.Options.Add(CommonOptions.OperatingSystemOption); - command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.TestRuntimeOptionDescription)); - command.Options.Add(VerbosityOption); - command.Options.Add(CommonOptions.NoRestoreOption); - command.Options.Add(SelfContainedOption); - command.Options.Add(NoSelfContainedOption); - command.Options.Add(MicrosoftTestingPlatformOptions.NoBuildOption); - command.Options.Add(MicrosoftTestingPlatformOptions.NoAnsiOption); - command.Options.Add(MicrosoftTestingPlatformOptions.NoProgressOption); - command.Options.Add(MicrosoftTestingPlatformOptions.OutputOption); - command.Options.Add(MicrosoftTestingPlatformOptions.ListTestsOption); - command.Options.Add(MicrosoftTestingPlatformOptions.NoLaunchProfileOption); - command.Options.Add(MicrosoftTestingPlatformOptions.NoLaunchProfileArgumentsOption); - command.Options.Add(MTPTargetOption); - return command; } - private static Command GetVSTestCliCommand() + private static Command CreateVSTestCommand() { - Command command = new("test", CliCommandStrings.DotnetTestCommandVSTestDescription) - { - TreatUnmatchedTokensAsErrors = false, - DocsLink = DocsLink - }; - - // We are on purpose not capturing the solution, project or directory here. We want to pass it to the - // MSBuild command so we are letting it flow. - - command.Options.Add(SettingsOption); - command.Options.Add(ListTestsOption); - command.Options.Add(CommonOptions.TestEnvOption); - command.Options.Add(FilterOption); - command.Options.Add(AdapterOption); - command.Options.Add(LoggerOption); - command.Options.Add(OutputOption); - command.Options.Add(CommonOptions.ArtifactsPathOption); - command.Options.Add(DiagOption); - command.Options.Add(NoBuildOption); - command.Options.Add(ResultsOption); - command.Options.Add(CollectOption); - command.Options.Add(BlameOption); - command.Options.Add(BlameCrashOption); - command.Options.Add(BlameCrashDumpOption); - command.Options.Add(BlameCrashAlwaysOption); - command.Options.Add(BlameHangOption); - command.Options.Add(BlameHangDumpOption); - command.Options.Add(BlameHangTimeoutOption); - command.Options.Add(NoLogoOption); - command.Options.Add(ConfigurationOption); - command.Options.Add(FrameworkOption); - command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.TestRuntimeOptionDescription)); - command.Options.Add(NoRestoreOption); - command.Options.Add(SelfContainedOption); - command.Options.Add(NoSelfContainedOption); - command.Options.Add(CommonOptions.InteractiveMsBuildForwardOption); - command.Options.Add(VerbosityOption); - command.Options.Add(CommonOptions.ArchitectureOption); - command.Options.Add(CommonOptions.OperatingSystemOption); - command.Options.Add(CommonOptions.PropertiesOption); - command.Options.Add(CommonOptions.DisableBuildServersOption); - command.Options.Add(VsTestTargetOption); + var command = new Command(TestCommandDefinition.Name); + TestCommandDefinition.ConfigureVSTestCommand(command); command.SetAction(TestCommand.Run); - return command; } - - private static string GetSemiColonEscapedstring(string arg) - { - if (arg.IndexOf(";") != -1) - { - return arg.Replace(";", "%3b"); - } - - return arg; - } - - private static string[] GetSemiColonEscapedArgs(IEnumerable args) - { - int counter = 0; - string[] array = new string[args.Count()]; - - foreach (string arg in args) - { - array[counter++] = GetSemiColonEscapedstring(arg); - } - - return array; - } - - /// - /// Adding double quotes around the property helps MSBuild arguments parser and avoid incorrect splits on ',' or ';'. - /// - internal /* for testing purposes */ static string SurroundWithDoubleQuotes(string input) - { - if (input is null) - { - throw new ArgumentNullException(nameof(input)); - } - - // If already escaped by double quotes then return original string. - if (input.StartsWith("\"", StringComparison.Ordinal) - && input.EndsWith("\"", StringComparison.Ordinal)) - { - return input; - } - - // We want to count the number of trailing backslashes to ensure - // we will have an even number before adding the final double quote. - // Otherwise the last \" will be interpreted as escaping the double - // quote rather than a backslash and a double quote. - var trailingBackslashesCount = 0; - for (int i = input.Length - 1; i >= 0; i--) - { - if (input[i] == '\\') - { - trailingBackslashesCount++; - } - else - { - break; - } - } - - return trailingBackslashesCount % 2 == 0 - ? string.Concat("\"", input, "\"") - : string.Concat("\"", input, "\\\""); - } } diff --git a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs index 77e434fb60fd..18a7d8045056 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs @@ -24,8 +24,8 @@ public static int Run(ParseResult parseResult) parseResult.HandleDebugSwitch(); CommonOptions.ValidateSelfContainedOptions( - parseResult.HasOption(TestCommandParser.SelfContainedOption), - parseResult.HasOption(TestCommandParser.NoSelfContainedOption)); + parseResult.HasOption(TestCommandDefinition.SelfContainedOption), + parseResult.HasOption(TestCommandDefinition.NoSelfContainedOption)); FeatureFlag.Instance.PrintFlagFeatureState(); @@ -237,14 +237,14 @@ private static TestCommand FromParseResult(ParseResult result, string[] settings msbuildArgs.Add($"-property:VSTestSessionCorrelationId={testSessionCorrelationId}"); } - bool noRestore = result.GetValue(TestCommandParser.NoRestoreOption) || result.GetValue(TestCommandParser.NoBuildOption); + bool noRestore = result.GetValue(TestCommandDefinition.NoRestoreOption) || result.GetValue(TestCommandDefinition.NoBuildOption); var parsedMSBuildArgs = MSBuildArgs.AnalyzeMSBuildArguments( msbuildArgs, CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, - TestCommandParser.VsTestTargetOption, - TestCommandParser.VerbosityOption, + TestCommandDefinition.VsTestTargetOption, + TestCommandDefinition.VerbosityOption, CommonOptions.NoLogoOption()) .CloneWithNoLogo(true); @@ -292,9 +292,9 @@ internal static int RunArtifactPostProcessingIfNeeded(string testSessionCorrelat var artifactsPostProcessArgs = new List { "--artifactsProcessingMode-postprocess", $"--testSessionCorrelationId:{testSessionCorrelationId}" }; - if (parseResult.GetResult(TestCommandParser.DiagOption) is not null) + if (parseResult.GetResult(TestCommandDefinition.DiagOption) is not null) { - artifactsPostProcessArgs.Add($"--diag:{parseResult.GetValue(TestCommandParser.DiagOption)}"); + artifactsPostProcessArgs.Add($"--diag:{parseResult.GetValue(TestCommandDefinition.DiagOption)}"); } try diff --git a/src/Cli/dotnet/Extensions/OptionForwardingExtensions.cs b/src/Cli/dotnet/Extensions/OptionForwardingExtensions.cs index 83eabb7e7675..6d191c02f8f3 100644 --- a/src/Cli/dotnet/Extensions/OptionForwardingExtensions.cs +++ b/src/Cli/dotnet/Extensions/OptionForwardingExtensions.cs @@ -30,7 +30,7 @@ public static Option ForwardAsOutputPath(this Option option, str { // Not sure if this is necessary, but this is what "dotnet test" previously did and so we are // preserving the behavior here after refactoring - argVal = TestCommandParser.SurroundWithDoubleQuotes(argVal); + argVal = TestCommandDefinition.SurroundWithDoubleQuotes(argVal); } return [ $"--property:{outputPropertyName}={argVal}", diff --git a/src/Cli/dotnet/Telemetry/TelemetryFilter.cs b/src/Cli/dotnet/Telemetry/TelemetryFilter.cs index f410cd70c2fa..730daae1ed57 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryFilter.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryFilter.cs @@ -118,8 +118,8 @@ PublishCommandParser.ConfigurationOption ] ( topLevelCommandName: ["run", "clean", "test"], optionsToLog: [ RunCommandParser.FrameworkOption, CleanCommandParser.FrameworkOption, - TestCommandParser.FrameworkOption, RunCommandParser.ConfigurationOption, CleanCommandParser.ConfigurationOption, - TestCommandParser.ConfigurationOption ] + TestCommandDefinition.FrameworkOption, RunCommandParser.ConfigurationOption, CleanCommandParser.ConfigurationOption, + TestCommandDefinition.ConfigurationOption ] ), new TopLevelCommandNameAndOptionToLog ( diff --git a/test/dotnet.Tests/CommandTests/Test/TestCommandParserTests.cs b/test/dotnet.Tests/CommandTests/Test/TestCommandParserTests.cs index 1c780c37bc14..913acc7f8cd7 100644 --- a/test/dotnet.Tests/CommandTests/Test/TestCommandParserTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/TestCommandParserTests.cs @@ -13,7 +13,7 @@ public class TestCommandParserTests public void SurroundWithDoubleQuotesWithNullThrows() { Assert.Throws(() => - TestCommandParser.SurroundWithDoubleQuotes(null!)); + TestCommandDefinition.SurroundWithDoubleQuotes(null!)); } [Theory] @@ -23,7 +23,7 @@ public void SurroundWithDoubleQuotesWithNullThrows() public void SurroundWithDoubleQuotesWhenAlreadySurroundedDoesNothing(string input) { var escapedInput = "\"" + input + "\""; - var result = TestCommandParser.SurroundWithDoubleQuotes(escapedInput); + var result = TestCommandDefinition.SurroundWithDoubleQuotes(escapedInput); result.Should().Be(escapedInput); } @@ -35,7 +35,7 @@ public void SurroundWithDoubleQuotesWhenAlreadySurroundedDoesNothing(string inpu [InlineData("a\"")] public void SurroundWithDoubleQuotesWhenNotSurroundedSurrounds(string input) { - var result = TestCommandParser.SurroundWithDoubleQuotes(input); + var result = TestCommandDefinition.SurroundWithDoubleQuotes(input); result.Should().Be("\"" + input + "\""); } @@ -46,7 +46,7 @@ public void SurroundWithDoubleQuotesWhenNotSurroundedSurrounds(string input) [InlineData("/\\/\\/\\\\")] public void SurroundWithDoubleQuotesHandlesCorrectlyEvenCountOfTrailingBackslashes(string input) { - var result = TestCommandParser.SurroundWithDoubleQuotes(input); + var result = TestCommandDefinition.SurroundWithDoubleQuotes(input); result.Should().Be("\"" + input + "\""); } @@ -57,7 +57,7 @@ public void SurroundWithDoubleQuotesHandlesCorrectlyEvenCountOfTrailingBackslash [InlineData("/\\/\\/\\")] public void SurroundWithDoubleQuotesHandlesCorrectlyOddCountOfTrailingBackslashes(string input) { - var result = TestCommandParser.SurroundWithDoubleQuotes(input); + var result = TestCommandDefinition.SurroundWithDoubleQuotes(input); result.Should().Be("\"" + input + "\\\""); } From e8687e669928f20fc45abd954e2cadf1e49ad6ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:24:18 +0000 Subject: [PATCH 4/4] Restrict --self-contained options to MTP only Per review feedback: - Moved SelfContainedOption and NoSelfContainedOption to MicrosoftTestingPlatformOptions - Removed these options from ConfigureVSTestCommand (VSTest) - Removed validation from TestCommand.cs (VSTest) - Updated MicrosoftTestingPlatformTestCommand.cs to reference MicrosoftTestingPlatformOptions - Removed VSTest-specific tests for these options since they're now MTP-only Co-authored-by: marcpopMSFT <12663534+marcpopMSFT@users.noreply.github.com> --- .../MTP/MicrosoftTestingPlatformOptions.cs | 4 + .../MicrosoftTestingPlatformTestCommand.cs | 4 +- .../Commands/Test/TestCommandDefinition.cs | 10 +-- .../Commands/Test/VSTest/TestCommand.cs | 4 - ...enDotnetTestBuildsAndRunsTestfromCsproj.cs | 81 ------------------- 5 files changed, 8 insertions(+), 95 deletions(-) diff --git a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformOptions.cs b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformOptions.cs index f7678ca2b4f2..1c202a252667 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformOptions.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformOptions.cs @@ -109,6 +109,10 @@ internal static class MicrosoftTestingPlatformOptions Description = CliCommandStrings.CmdListTestsDescription, Arity = ArgumentArity.Zero }; + + public static readonly Option SelfContainedOption = CommonOptions.SelfContainedOption; + + public static readonly Option NoSelfContainedOption = CommonOptions.NoSelfContainedOption; } internal enum OutputOptions diff --git a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs index 44e739d91910..1c53e9e898e6 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs @@ -41,8 +41,8 @@ private int RunInternal(ParseResult parseResult, bool isHelp) ValidationUtility.ValidateSolutionOrProjectOrDirectoryOrModulesArePassedCorrectly(parseResult); CommonOptions.ValidateSelfContainedOptions( - parseResult.HasOption(TestCommandDefinition.SelfContainedOption), - parseResult.HasOption(TestCommandDefinition.NoSelfContainedOption)); + parseResult.HasOption(MicrosoftTestingPlatformOptions.SelfContainedOption), + parseResult.HasOption(MicrosoftTestingPlatformOptions.NoSelfContainedOption)); int degreeOfParallelism = GetDegreeOfParallelism(parseResult); var testOptions = new TestOptions( diff --git a/src/Cli/dotnet/Commands/Test/TestCommandDefinition.cs b/src/Cli/dotnet/Commands/Test/TestCommandDefinition.cs index a0de338d2703..ba0a7b0cce7c 100644 --- a/src/Cli/dotnet/Commands/Test/TestCommandDefinition.cs +++ b/src/Cli/dotnet/Commands/Test/TestCommandDefinition.cs @@ -165,10 +165,6 @@ private static Option CreateBlameHangDumpOption() public static readonly Option NoRestoreOption = CommonOptions.NoRestoreOption; - public static readonly Option SelfContainedOption = CommonOptions.SelfContainedOption; - - public static readonly Option NoSelfContainedOption = CommonOptions.NoSelfContainedOption; - public static readonly Option FrameworkOption = CommonOptions.FrameworkOption(CliCommandStrings.TestFrameworkOptionDescription); public static readonly Option ConfigurationOption = CommonOptions.ConfigurationOption(CliCommandStrings.TestConfigurationOptionDescription); @@ -269,8 +265,8 @@ public static void ConfigureTestingPlatformCommand(Command command) command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.TestRuntimeOptionDescription)); command.Options.Add(VerbosityOption); command.Options.Add(CommonOptions.NoRestoreOption); - command.Options.Add(SelfContainedOption); - command.Options.Add(NoSelfContainedOption); + command.Options.Add(MicrosoftTestingPlatformOptions.SelfContainedOption); + command.Options.Add(MicrosoftTestingPlatformOptions.NoSelfContainedOption); command.Options.Add(MicrosoftTestingPlatformOptions.NoBuildOption); command.Options.Add(MicrosoftTestingPlatformOptions.NoAnsiOption); command.Options.Add(MicrosoftTestingPlatformOptions.NoProgressOption); @@ -314,8 +310,6 @@ public static void ConfigureVSTestCommand(Command command) command.Options.Add(FrameworkOption); command.Options.Add(CommonOptions.RuntimeOption(CliCommandStrings.TestRuntimeOptionDescription)); command.Options.Add(NoRestoreOption); - command.Options.Add(SelfContainedOption); - command.Options.Add(NoSelfContainedOption); command.Options.Add(CommonOptions.InteractiveMsBuildForwardOption); command.Options.Add(VerbosityOption); command.Options.Add(CommonOptions.ArchitectureOption); diff --git a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs index 18a7d8045056..32bc8cf23172 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs @@ -23,10 +23,6 @@ public static int Run(ParseResult parseResult) { parseResult.HandleDebugSwitch(); - CommonOptions.ValidateSelfContainedOptions( - parseResult.HasOption(TestCommandDefinition.SelfContainedOption), - parseResult.HasOption(TestCommandDefinition.NoSelfContainedOption)); - FeatureFlag.Instance.PrintFlagFeatureState(); // We use also current process id for the correlation id for possible future usage in case we need to know the parent process diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs index c91c2a033303..6c054f61ceed 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs @@ -836,87 +836,6 @@ public void PropertiesEndingWithDotDllShouldNotFail(string property) result.ExitCode.Should().Be(1); } - [Fact] - public void ItTestsWithSelfContainedOption() - { - var testInstance = _testAssetsManager.CopyTestAsset("XunitCore") - .WithSource() - .WithVersionVariables(); - - var rootPath = testInstance.Path; - var rid = EnvironmentInfo.GetCompatibleRid(); - - var result = new DotnetTestCommand(Log, disableNewOutput: true, ConsoleLoggerOutputNormal) - .WithWorkingDirectory(rootPath) - .Execute("--runtime", rid, "--self-contained"); - - result - .Should() - .NotHaveStdErrContaining("NETSDK1179") - .And - .NotHaveStdErrContaining("MSB1001"); - - if (!TestContext.IsLocalized()) - { - result.StdOut.Should().Contain("Total tests: 2"); - result.StdOut.Should().Contain("Passed: 1"); - result.StdOut.Should().Contain("Failed: 1"); - } - } - - [Fact] - public void ItTestsWithNoSelfContainedOption() - { - var testInstance = _testAssetsManager.CopyTestAsset("XunitCore") - .WithSource() - .WithVersionVariables(); - - var rootPath = testInstance.Path; - var rid = EnvironmentInfo.GetCompatibleRid(); - - var result = new DotnetTestCommand(Log, disableNewOutput: true, ConsoleLoggerOutputNormal) - .WithWorkingDirectory(rootPath) - .Execute("--runtime", rid, "--no-self-contained"); - - result - .Should() - .NotHaveStdErrContaining("NETSDK1179") - .And - .NotHaveStdErrContaining("MSB1001"); - - if (!TestContext.IsLocalized()) - { - result.StdOut.Should().Contain("Total tests: 2"); - result.StdOut.Should().Contain("Passed: 1"); - result.StdOut.Should().Contain("Failed: 1"); - } - } - - [Fact] - public void ItFailsWhenBothSelfContainedAndNoSelfContainedAreSpecified() - { - var testInstance = _testAssetsManager.CopyTestAsset("XunitCore") - .WithSource() - .WithVersionVariables(); - - var rootPath = testInstance.Path; - var rid = EnvironmentInfo.GetCompatibleRid(); - - var result = new DotnetTestCommand(Log, disableNewOutput: true, ConsoleLoggerOutputNormal) - .WithWorkingDirectory(rootPath) - .Execute("--runtime", rid, "--self-contained", "--no-self-contained"); - - result - .Should() - .Fail(); - - if (!TestContext.IsLocalized()) - { - result.StdErr.Should().Contain("--self-contained") - .And.Contain("--no-self-contained"); - } - } - [Fact] public void DistributedLoggerEndingWithDotDllShouldBePassedToMSBuild() {