Skip to content

Commit 74b4ee0

Browse files
committed
[MTP dotnet test]: Allow specifying project, solution, directory, or test modules as positional argument
1 parent 1b58d76 commit 74b4ee0

File tree

6 files changed

+111
-76
lines changed

6 files changed

+111
-76
lines changed

src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ public static BuildOptions GetBuildOptions(ParseResult parseResult)
101101
{
102102
LoggerUtility.SeparateBinLogArguments(parseResult.UnmatchedTokens, out var binLogArgs, out var otherArgs);
103103

104+
var (positionalProjectOrSolution, positionalTestModules) = GetPositionalArguments(otherArgs);
105+
ValidatePathOptions(parseResult, positionalProjectOrSolution, positionalTestModules);
106+
104107
var msbuildArgs = parseResult.OptionValuesToBeForwarded(TestCommandParser.GetCommand())
105108
.Concat(binLogArgs);
106109

@@ -123,8 +126,9 @@ public static BuildOptions GetBuildOptions(ParseResult parseResult)
123126
}
124127

125128
PathOptions pathOptions = new(
126-
parseResult.GetValue(MicrosoftTestingPlatformOptions.ProjectOrSolutionOption),
129+
positionalProjectOrSolution ?? parseResult.GetValue(MicrosoftTestingPlatformOptions.ProjectOrSolutionOption),
127130
parseResult.GetValue(MicrosoftTestingPlatformOptions.SolutionOption),
131+
positionalTestModules ?? parseResult.GetValue(MicrosoftTestingPlatformOptions.TestModulesFilterOption),
128132
resultsDirectory,
129133
configFile,
130134
diagnosticOutputDirectory);
@@ -140,6 +144,100 @@ public static BuildOptions GetBuildOptions(ParseResult parseResult)
140144
msbuildArgs);
141145
}
142146

147+
private static void ValidatePathOptions(ParseResult parseResult, string? positionalProjectOrSolution, string? positionalTestModules)
148+
{
149+
var count = 0;
150+
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.TestModulesFilterOption))
151+
count++;
152+
153+
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.SolutionOption))
154+
count++;
155+
156+
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.ProjectOrSolutionOption))
157+
count++;
158+
159+
if (positionalProjectOrSolution is not null)
160+
count++;
161+
162+
if (positionalTestModules is not null)
163+
count++;
164+
165+
if (count > 1)
166+
throw new GracefulException(CliCommandStrings.CmdMultipleBuildPathOptionsErrorDescription);
167+
}
168+
169+
private static (string? PositionalProjectOrSolution, string? PositionalTestModules) GetPositionalArguments(List<string> otherArgs)
170+
{
171+
string? positionalProjectOrSolution = null;
172+
string? positionalTestModules = null;
173+
174+
// In case there is a valid case, users can opt-out.
175+
// Note that the validation here is added to have a "better" error message for scenarios that will already fail.
176+
// So, disabling validation is okay if the user scenario is valid.
177+
bool throwOnUnexpectedFilePassedAsNonFirstPositionalArgument = Environment.GetEnvironmentVariable("DOTNET_TEST_DISABLE_SWITCH_VALIDATION") is not ("true" or "1");
178+
179+
for (int i = 0; i < otherArgs.Count; i++)
180+
{
181+
var token = otherArgs[i];
182+
if ((token.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) ||
183+
token.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase)) && File.Exists(token))
184+
{
185+
if (i == 0)
186+
{
187+
positionalProjectOrSolution = token;
188+
otherArgs.RemoveAt(0);
189+
}
190+
else if (throwOnUnexpectedFilePassedAsNonFirstPositionalArgument)
191+
{
192+
throw new GracefulException(CliCommandStrings.TestCommandUseSolution);
193+
}
194+
}
195+
else if ((token.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) ||
196+
token.EndsWith(".vbproj", StringComparison.OrdinalIgnoreCase) ||
197+
token.EndsWith(".fsproj", StringComparison.OrdinalIgnoreCase)) && File.Exists(token))
198+
{
199+
if (i == 0)
200+
{
201+
positionalProjectOrSolution = token;
202+
otherArgs.RemoveAt(0);
203+
}
204+
else if (throwOnUnexpectedFilePassedAsNonFirstPositionalArgument)
205+
{
206+
throw new GracefulException(CliCommandStrings.TestCommandUseProject);
207+
}
208+
}
209+
else if ((token.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
210+
token.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) &&
211+
File.Exists(token))
212+
{
213+
if (i == 0)
214+
{
215+
positionalTestModules = token;
216+
otherArgs.RemoveAt(0);
217+
}
218+
else if (throwOnUnexpectedFilePassedAsNonFirstPositionalArgument)
219+
{
220+
throw new GracefulException(CliCommandStrings.TestCommandUseTestModules);
221+
}
222+
}
223+
else if (Directory.Exists(token))
224+
{
225+
if (i == 0)
226+
{
227+
positionalTestModules = token;
228+
otherArgs.RemoveAt(0);
229+
}
230+
else if (throwOnUnexpectedFilePassedAsNonFirstPositionalArgument)
231+
{
232+
throw new GracefulException(CliCommandStrings.TestCommandUseDirectoryWithSwitch);
233+
}
234+
}
235+
}
236+
237+
return (positionalProjectOrSolution, positionalTestModules);
238+
}
239+
240+
143241
private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOptions buildOptions)
144242
{
145243
if (buildOptions.HasNoBuild)

src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,23 @@ public int Run(ParseResult parseResult, bool isHelp = false)
3737

3838
private int RunInternal(ParseResult parseResult, bool isHelp)
3939
{
40-
ValidationUtility.ValidateMutuallyExclusiveOptions(parseResult);
41-
ValidationUtility.ValidateSolutionOrProjectOrDirectoryOrModulesArePassedCorrectly(parseResult);
40+
BuildOptions buildOptions = MSBuildUtility.GetBuildOptions(parseResult);
41+
ValidationUtility.ValidateMutuallyExclusiveOptions(parseResult, buildOptions.PathOptions);
4242

4343
int degreeOfParallelism = GetDegreeOfParallelism(parseResult);
4444
var testOptions = new TestOptions(
4545
IsHelp: isHelp,
4646
IsDiscovery: parseResult.HasOption(MicrosoftTestingPlatformOptions.ListTestsOption),
4747
EnvironmentVariables: parseResult.GetValue(CommonOptions.EnvOption) ?? ImmutableDictionary<string, string>.Empty);
4848

49-
BuildOptions buildOptions = MSBuildUtility.GetBuildOptions(parseResult);
50-
51-
bool filterModeEnabled = parseResult.HasOption(MicrosoftTestingPlatformOptions.TestModulesFilterOption);
5249
TestApplicationActionQueue actionQueue;
53-
if (filterModeEnabled)
50+
if (buildOptions.PathOptions.TestModules is not null)
5451
{
5552
InitializeOutput(degreeOfParallelism, parseResult, testOptions);
5653

5754
actionQueue = new TestApplicationActionQueue(degreeOfParallelism, buildOptions, testOptions, _output, OnHelpRequested);
5855
var testModulesFilterHandler = new TestModulesFilterHandler(actionQueue, _output);
59-
if (!testModulesFilterHandler.RunWithTestModulesFilter(parseResult))
56+
if (!testModulesFilterHandler.RunWithTestModulesFilter(parseResult, buildOptions.PathOptions.TestModules))
6057
{
6158
return ExitCode.GenericFailure;
6259
}

src/Cli/dotnet/Commands/Test/MTP/Options.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Microsoft.DotNet.Cli.Commands.Test;
55

66
internal record TestOptions(bool IsHelp, bool IsDiscovery, IReadOnlyDictionary<string, string> EnvironmentVariables);
77

8-
internal record PathOptions(string? ProjectOrSolutionPath, string? SolutionPath, string? ResultsDirectoryPath, string? ConfigFilePath, string? DiagnosticOutputDirectoryPath);
8+
internal record PathOptions(string? ProjectOrSolutionPath, string? SolutionPath, string? TestModules, string? ResultsDirectoryPath, string? ConfigFilePath, string? DiagnosticOutputDirectoryPath);
99

1010
internal record BuildOptions(
1111
PathOptions PathOptions,
@@ -14,5 +14,5 @@ internal record BuildOptions(
1414
Utils.VerbosityOptions? Verbosity,
1515
bool NoLaunchProfile,
1616
bool NoLaunchProfileArguments,
17-
List<string> UnmatchedTokens,
17+
List<string> TestApplicationArguments,
1818
IEnumerable<string> MSBuildArgs);

src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ private string GetArguments()
168168
builder.Append($" {MicrosoftTestingPlatformOptions.DiagnosticOutputDirectoryOption.Name} {ArgumentEscaper.EscapeSingleArg(diagnosticOutputDirectoryPath)}");
169169
}
170170

171-
foreach (var arg in _buildOptions.UnmatchedTokens)
171+
foreach (var arg in _buildOptions.TestApplicationArguments)
172172
{
173173
builder.Append($" {ArgumentEscaper.EscapeSingleArg(arg)}");
174174
}

src/Cli/dotnet/Commands/Test/MTP/TestModulesFilterHandler.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@ internal sealed class TestModulesFilterHandler(TestApplicationActionQueue action
1616
private readonly TestApplicationActionQueue _actionQueue = actionQueue;
1717
private readonly TerminalTestReporter _output = output;
1818

19-
public bool RunWithTestModulesFilter(ParseResult parseResult)
19+
public bool RunWithTestModulesFilter(ParseResult parseResult, string testModules)
2020
{
2121
// If the module path pattern(s) was provided, we will use that to filter the test modules
22-
string testModules = parseResult.GetValue(MicrosoftTestingPlatformOptions.TestModulesFilterOption)!;
23-
2422
// If the root directory was provided, we will use that to search for the test modules
2523
// Otherwise, we will use the current directory
2624
string? rootDirectory = Directory.GetCurrentDirectory();

src/Cli/dotnet/Commands/Test/MTP/ValidationUtility.cs

Lines changed: 4 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,13 @@ namespace Microsoft.DotNet.Cli.Commands.Test;
1111

1212
internal static class ValidationUtility
1313
{
14-
public static void ValidateMutuallyExclusiveOptions(ParseResult parseResult)
14+
public static void ValidateMutuallyExclusiveOptions(ParseResult parseResult, PathOptions pathOptions)
1515
{
16-
ValidatePathOptions(parseResult);
17-
ValidateOptionsIrrelevantToModulesFilter(parseResult);
16+
ValidateOptionsIrrelevantToModulesFilter(parseResult, pathOptions);
1817

19-
static void ValidatePathOptions(ParseResult parseResult)
18+
static void ValidateOptionsIrrelevantToModulesFilter(ParseResult parseResult, PathOptions pathOptions)
2019
{
21-
var count = 0;
22-
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.TestModulesFilterOption))
23-
count++;
24-
25-
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.SolutionOption))
26-
count++;
27-
28-
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.ProjectOrSolutionOption))
29-
count++;
30-
31-
if (count > 1)
32-
throw new GracefulException(CliCommandStrings.CmdMultipleBuildPathOptionsErrorDescription);
33-
}
34-
35-
static void ValidateOptionsIrrelevantToModulesFilter(ParseResult parseResult)
36-
{
37-
if (!parseResult.HasOption(MicrosoftTestingPlatformOptions.TestModulesFilterOption))
20+
if (pathOptions.TestModules is null)
3821
{
3922
return;
4023
}
@@ -95,47 +78,6 @@ private static bool TryGetProjectOrSolutionFromDirectory(
9578
return true;
9679
}
9780

98-
/// <summary>
99-
/// Validates that arguments requiring specific command-line switches are used correctly for Microsoft.Testing.Platform.
100-
/// Provides helpful error messages when users provide file/directory arguments without proper switches.
101-
/// </summary>
102-
public static void ValidateSolutionOrProjectOrDirectoryOrModulesArePassedCorrectly(ParseResult parseResult)
103-
{
104-
if (Environment.GetEnvironmentVariable("DOTNET_TEST_DISABLE_SWITCH_VALIDATION") is "true" or "1")
105-
{
106-
// In case there is a valid case, users can opt-out.
107-
// Note that the validation here is added to have a "better" error message for scenarios that will already fail.
108-
// So, disabling validation is okay if the user scenario is valid.
109-
return;
110-
}
111-
112-
foreach (string token in parseResult.UnmatchedTokens)
113-
{
114-
// Check for .sln files
115-
if ((token.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) ||
116-
token.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase)) && File.Exists(token))
117-
{
118-
throw new GracefulException(CliCommandStrings.TestCommandUseSolution);
119-
}
120-
else if ((token.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) ||
121-
token.EndsWith(".vbproj", StringComparison.OrdinalIgnoreCase) ||
122-
token.EndsWith(".fsproj", StringComparison.OrdinalIgnoreCase)) && File.Exists(token))
123-
{
124-
throw new GracefulException(CliCommandStrings.TestCommandUseProject);
125-
}
126-
else if ((token.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
127-
token.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) &&
128-
File.Exists(token))
129-
{
130-
throw new GracefulException(CliCommandStrings.TestCommandUseTestModules);
131-
}
132-
else if (Directory.Exists(token))
133-
{
134-
throw new GracefulException(CliCommandStrings.TestCommandUseDirectoryWithSwitch);
135-
}
136-
}
137-
}
138-
13981
private static bool ValidateSolutionPath(string solutionFileOrDirectory, [NotNullWhen(true)] out string? solutionFile)
14082
{
14183
// If it's a directory, just check if it exists

0 commit comments

Comments
 (0)