Skip to content
1 change: 1 addition & 0 deletions src/Cli/dotnet/Commands/Test/CliConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.DotNet.Cli.Commands.Test;
internal static class CliConstants
{
public const string ServerOptionKey = "--server";
public const string HelpOptionKey = "--help";
public const string DotNetTestPipeOptionKey = "--dotnet-test-pipe";

public const string ServerOptionValue = "dotnettestcli";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,38 +66,38 @@ public static BuildOptions GetBuildOptions(ParseResult parseResult, int degreeOf
var msbuildArgs = parseResult.OptionValuesToBeForwarded(TestCommandParser.GetCommand())
.Concat(binLogArgs);

string? resultsDirectory = parseResult.GetValue(TestingPlatformOptions.ResultsDirectoryOption);
string? resultsDirectory = parseResult.GetValue(MicrosoftTestingPlatformOptions.ResultsDirectoryOption);
if (resultsDirectory is not null)
{
resultsDirectory = Path.GetFullPath(resultsDirectory);
}

string? configFile = parseResult.GetValue(TestingPlatformOptions.ConfigFileOption);
string? configFile = parseResult.GetValue(MicrosoftTestingPlatformOptions.ConfigFileOption);
if (configFile is not null)
{
configFile = Path.GetFullPath(configFile);
}

string? diagnosticOutputDirectory = parseResult.GetValue(TestingPlatformOptions.DiagnosticOutputDirectoryOption);
string? diagnosticOutputDirectory = parseResult.GetValue(MicrosoftTestingPlatformOptions.DiagnosticOutputDirectoryOption);
if (diagnosticOutputDirectory is not null)
{
diagnosticOutputDirectory = Path.GetFullPath(diagnosticOutputDirectory);
}

PathOptions pathOptions = new(
parseResult.GetValue(TestingPlatformOptions.ProjectOption),
parseResult.GetValue(TestingPlatformOptions.SolutionOption),
parseResult.GetValue(MicrosoftTestingPlatformOptions.ProjectOption),
parseResult.GetValue(MicrosoftTestingPlatformOptions.SolutionOption),
resultsDirectory,
configFile,
diagnosticOutputDirectory);

return new BuildOptions(
pathOptions,
parseResult.GetValue(CommonOptions.NoRestoreOption),
parseResult.GetValue(TestingPlatformOptions.NoBuildOption),
parseResult.GetValue(MicrosoftTestingPlatformOptions.NoBuildOption),
parseResult.HasOption(TestCommandParser.VerbosityOption) ? parseResult.GetValue(TestCommandParser.VerbosityOption) : null,
parseResult.GetValue(TestingPlatformOptions.NoLaunchProfileOption),
parseResult.GetValue(TestingPlatformOptions.NoLaunchProfileArgumentsOption),
parseResult.GetValue(MicrosoftTestingPlatformOptions.NoLaunchProfileOption),
parseResult.GetValue(MicrosoftTestingPlatformOptions.NoLaunchProfileArgumentsOption),
degreeOfParallelism,
otherArgs,
msbuildArgs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Microsoft.DotNet.Cli.Commands.Test;

internal static class TestingPlatformOptions
internal static class MicrosoftTestingPlatformOptions
{
public static readonly Option<string> ProjectOption = new("--project")
{
Expand Down Expand Up @@ -111,11 +111,6 @@ internal static class TestingPlatformOptions
Description = CliCommandStrings.CmdListTestsDescription,
Arity = ArgumentArity.Zero
};

public static readonly Option<string> HelpOption = new("--help", ["-h", "-?"])
{
Arity = ArgumentArity.Zero
};
}

internal enum OutputOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Microsoft.DotNet.Cli.Commands.Test;

internal partial class TestingPlatformCommand
internal partial class MicrosoftTestingPlatformTestCommand
{
private readonly ConcurrentDictionary<string, CommandLineOption> _commandLineOptionNameToModuleNames = [];
private readonly ConcurrentDictionary<bool, List<(string, string[])>> _moduleNamesToCommandLineOptions = [];
Expand All @@ -22,7 +22,7 @@ public IEnumerable<Action<HelpContext>> CustomHelpLayout()
WriteHelpOptions(context);
Console.WriteLine(CliCommandStrings.HelpWaitingForOptionsAndExtensions);

Run(context.ParseResult);
Run(context.ParseResult, isHelp: true);

if (_commandLineOptionNameToModuleNames.IsEmpty)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,24 @@

namespace Microsoft.DotNet.Cli.Commands.Test;

internal partial class TestingPlatformCommand : Command, ICustomHelp
internal partial class MicrosoftTestingPlatformTestCommand : Command, ICustomHelp, ICommandDocument
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional interface is for #50638

{
private MSBuildHandler _msBuildHandler;
private TerminalTestReporter _output;
private TestApplicationActionQueue _actionQueue;

private byte _cancelled;
private bool _isDiscovery;
private bool _isRetry;

public TestingPlatformCommand(string name, string description = null) : base(name, description)
public MicrosoftTestingPlatformTestCommand(string name, string description = null) : base(name, description)
{
TreatUnmatchedTokensAsErrors = false;
}

public int Run(ParseResult parseResult)
public string DocsLink => "https://aka.ms/dotnet-test";

public int Run(ParseResult parseResult, bool isHelp = false)
{
int? exitCode = null;
try
{
exitCode = RunInternal(parseResult);
exitCode = RunInternal(parseResult, isHelp);
return exitCode.Value;
}
finally
Expand All @@ -39,49 +36,53 @@ public int Run(ParseResult parseResult)
}
}

private int RunInternal(ParseResult parseResult)
private int RunInternal(ParseResult parseResult, bool isHelp)
{
ValidationUtility.ValidateMutuallyExclusiveOptions(parseResult);
ValidationUtility.ValidateSolutionOrProjectOrDirectoryOrModulesArePassedCorrectly(parseResult);

PrepareEnvironment(parseResult, out TestOptions testOptions, out int degreeOfParallelism);
int degreeOfParallelism = GetDegreeOfParallelism(parseResult);
bool filterModeEnabled = parseResult.HasOption(MicrosoftTestingPlatformOptions.TestModulesFilterOption);
var testOptions = new TestOptions(filterModeEnabled, IsHelp: isHelp, IsDiscovery: parseResult.HasOption(MicrosoftTestingPlatformOptions.ListTestsOption));

InitializeOutput(degreeOfParallelism, parseResult, testOptions);

InitializeOutput(degreeOfParallelism, parseResult, testOptions.IsHelp);
SetupCancelKeyPressHandler();

BuildOptions buildOptions = MSBuildUtility.GetBuildOptions(parseResult, degreeOfParallelism);

InitializeActionQueue(degreeOfParallelism, testOptions, buildOptions);
var actionQueue = InitializeActionQueue(degreeOfParallelism, testOptions, buildOptions);

_msBuildHandler = new(buildOptions, _actionQueue, _output);
TestModulesFilterHandler testModulesFilterHandler = new(_actionQueue, _output);
var msBuildHandler = new MSBuildHandler(buildOptions, actionQueue, _output);

if (testOptions.HasFilterMode)
{
var testModulesFilterHandler = new TestModulesFilterHandler(actionQueue, _output);
if (!testModulesFilterHandler.RunWithTestModulesFilter(parseResult))
{
return ExitCode.GenericFailure;
}
}
else
{
if (!_msBuildHandler.RunMSBuild())
if (!msBuildHandler.RunMSBuild())
{
return ExitCode.GenericFailure;
}

if (!_msBuildHandler.EnqueueTestApplications())
if (!msBuildHandler.EnqueueTestApplications())
{
return ExitCode.GenericFailure;
}
}

_actionQueue.EnqueueCompleted();
actionQueue.EnqueueCompleted();
// Don't inline exitCode variable. We want to always call WaitAllActions first.
var exitCode = _actionQueue.WaitAllActions();
var exitCode = actionQueue.WaitAllActions();
exitCode = _output.HasHandshakeFailure ? ExitCode.GenericFailure : exitCode;
if (exitCode == ExitCode.Success &&
parseResult.HasOption(TestingPlatformOptions.MinimumExpectedTestsOption) &&
parseResult.GetValue(TestingPlatformOptions.MinimumExpectedTestsOption) is { } minimumExpectedTests &&
parseResult.HasOption(MicrosoftTestingPlatformOptions.MinimumExpectedTestsOption) &&
parseResult.GetValue(MicrosoftTestingPlatformOptions.MinimumExpectedTestsOption) is { } minimumExpectedTests &&
_output.TotalTests < minimumExpectedTests)
{
exitCode = ExitCode.MinimumExpectedTestsPolicyViolation;
Expand All @@ -90,26 +91,9 @@ private int RunInternal(ParseResult parseResult)
return exitCode;
}

private void PrepareEnvironment(ParseResult parseResult, out TestOptions testOptions, out int degreeOfParallelism)
private TestApplicationActionQueue InitializeActionQueue(int degreeOfParallelism, TestOptions testOptions, BuildOptions buildOptions)
{
SetupCancelKeyPressHandler();

degreeOfParallelism = GetDegreeOfParallelism(parseResult);

bool filterModeEnabled = parseResult.HasOption(TestingPlatformOptions.TestModulesFilterOption);

var arguments = parseResult.GetArguments();
testOptions = GetTestOptions(filterModeEnabled, isHelp: ContainsHelpOption(arguments));

_isDiscovery = ContainsListTestsOption(arguments);

// This is ugly, and we need to replace it by passing out some info from testing platform to inform us that some process level retry plugin is active.
_isRetry = arguments.Contains("--retry-failed-tests");
}

private void InitializeActionQueue(int degreeOfParallelism, TestOptions testOptions, BuildOptions buildOptions)
{
_actionQueue = new TestApplicationActionQueue(degreeOfParallelism, buildOptions, testOptions, _output, async (TestApplication testApp) =>
return new TestApplicationActionQueue(degreeOfParallelism, buildOptions, testOptions, _output, async (TestApplication testApp) =>
{
testApp.HelpRequested += OnHelpRequested;
return await testApp.RunAsync();
Expand All @@ -126,12 +110,12 @@ private void SetupCancelKeyPressHandler()
};
}

private void InitializeOutput(int degreeOfParallelism, ParseResult parseResult, bool isHelp)
private void InitializeOutput(int degreeOfParallelism, ParseResult parseResult, TestOptions testOptions)
{
var console = new SystemConsole();
var showPassedTests = parseResult.GetValue(TestingPlatformOptions.OutputOption) == OutputOptions.Detailed;
var noProgress = parseResult.HasOption(TestingPlatformOptions.NoProgressOption);
var noAnsi = parseResult.HasOption(TestingPlatformOptions.NoAnsiOption);
var showPassedTests = parseResult.GetValue(MicrosoftTestingPlatformOptions.OutputOption) == OutputOptions.Detailed;
var noProgress = parseResult.HasOption(MicrosoftTestingPlatformOptions.NoProgressOption);
var noAnsi = parseResult.HasOption(MicrosoftTestingPlatformOptions.NoAnsiOption);

// TODO: Replace this with proper CI detection that we already have in telemetry. https://github.com/microsoft/testfx/issues/5533#issuecomment-2838893327
bool inCI = string.Equals(Environment.GetEnvironmentVariable("TF_BUILD"), "true", StringComparison.OrdinalIgnoreCase) || string.Equals(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"), "true", StringComparison.OrdinalIgnoreCase);
Expand All @@ -144,33 +128,23 @@ private void InitializeOutput(int degreeOfParallelism, ParseResult parseResult,
UseCIAnsi = inCI,
ShowAssembly = true,
ShowAssemblyStartAndComplete = true,
MinimumExpectedTests = parseResult.GetValue(TestingPlatformOptions.MinimumExpectedTestsOption),
MinimumExpectedTests = parseResult.GetValue(MicrosoftTestingPlatformOptions.MinimumExpectedTestsOption),
});

_output.TestExecutionStarted(DateTimeOffset.Now, degreeOfParallelism, _isDiscovery, isHelp, _isRetry);
// This is ugly, and we need to replace it by passing out some info from testing platform to inform us that some process level retry plugin is active.
var isRetry = parseResult.GetArguments().Contains("--retry-failed-tests");

_output.TestExecutionStarted(DateTimeOffset.Now, degreeOfParallelism, testOptions.IsDiscovery, testOptions.IsHelp, isRetry);
}

private static int GetDegreeOfParallelism(ParseResult parseResult)
{
var degreeOfParallelism = parseResult.GetValue(TestingPlatformOptions.MaxParallelTestModulesOption);
var degreeOfParallelism = parseResult.GetValue(MicrosoftTestingPlatformOptions.MaxParallelTestModulesOption);
if (degreeOfParallelism <= 0)
degreeOfParallelism = Environment.ProcessorCount;
return degreeOfParallelism;
}

private static TestOptions GetTestOptions(bool hasFilterMode, bool isHelp) =>
new(hasFilterMode, isHelp);

private static bool ContainsHelpOption(IEnumerable<string> args)
{
return args.Contains(TestingPlatformOptions.HelpOption.Name) || TestingPlatformOptions.HelpOption.Aliases.Any(alias => args.Contains(alias));
}

private static bool ContainsListTestsOption(IEnumerable<string> args)
{
return args.Contains(TestingPlatformOptions.ListTestsOption.Name);
}

private void CompleteRun(int? exitCode)
{
if (Interlocked.CompareExchange(ref _cancelled, 1, 0) == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Microsoft.DotNet.Cli.Commands.Test;

internal record TestOptions(bool HasFilterMode, bool IsHelp);
internal record TestOptions(bool HasFilterMode, bool IsHelp, bool IsDiscovery);

internal record PathOptions(string? ProjectPath, string? SolutionPath, string? ResultsDirectoryPath, string? ConfigFilePath, string? DiagnosticOutputDirectoryPath);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,27 @@ private string GetArguments()

if (TestOptions.IsHelp)
{
builder.Append($" {TestingPlatformOptions.HelpOption.Name}");
builder.Append($" {CliConstants.HelpOptionKey}");
}

if (TestOptions.IsDiscovery)
{
builder.Append($" {MicrosoftTestingPlatformOptions.ListTestsOption.Name}");
}

if (_buildOptions.PathOptions.ResultsDirectoryPath is { } resultsDirectoryPath)
{
builder.Append($" {TestingPlatformOptions.ResultsDirectoryOption.Name} {ArgumentEscaper.EscapeSingleArg(resultsDirectoryPath)}");
builder.Append($" {MicrosoftTestingPlatformOptions.ResultsDirectoryOption.Name} {ArgumentEscaper.EscapeSingleArg(resultsDirectoryPath)}");
}

if (_buildOptions.PathOptions.ConfigFilePath is { } configFilePath)
{
builder.Append($" {TestingPlatformOptions.ConfigFileOption.Name} {ArgumentEscaper.EscapeSingleArg(configFilePath)}");
builder.Append($" {MicrosoftTestingPlatformOptions.ConfigFileOption.Name} {ArgumentEscaper.EscapeSingleArg(configFilePath)}");
}

if (_buildOptions.PathOptions.DiagnosticOutputDirectoryPath is { } diagnosticOutputDirectoryPath)
{
builder.Append($" {TestingPlatformOptions.DiagnosticOutputDirectoryOption.Name} {ArgumentEscaper.EscapeSingleArg(diagnosticOutputDirectoryPath)}");
builder.Append($" {MicrosoftTestingPlatformOptions.DiagnosticOutputDirectoryOption.Name} {ArgumentEscaper.EscapeSingleArg(diagnosticOutputDirectoryPath)}");
}

foreach (var arg in _buildOptions.UnmatchedTokens)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ internal sealed class TestModulesFilterHandler(TestApplicationActionQueue action
public bool RunWithTestModulesFilter(ParseResult parseResult)
{
// If the module path pattern(s) was provided, we will use that to filter the test modules
string testModules = parseResult.GetValue(TestingPlatformOptions.TestModulesFilterOption);
string testModules = parseResult.GetValue(MicrosoftTestingPlatformOptions.TestModulesFilterOption);

// If the root directory was provided, we will use that to search for the test modules
// Otherwise, we will use the current directory
string rootDirectory = Directory.GetCurrentDirectory();
if (parseResult.HasOption(TestingPlatformOptions.TestModulesRootDirectoryOption))
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.TestModulesRootDirectoryOption))
{
rootDirectory = parseResult.GetValue(TestingPlatformOptions.TestModulesRootDirectoryOption);
rootDirectory = parseResult.GetValue(MicrosoftTestingPlatformOptions.TestModulesRootDirectoryOption);

// If the root directory is not valid, we simply return
if (string.IsNullOrEmpty(rootDirectory) || !Directory.Exists(rootDirectory))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ public static void ValidateMutuallyExclusiveOptions(ParseResult parseResult)
static void ValidatePathOptions(ParseResult parseResult)
{
var count = 0;
if (parseResult.HasOption(TestingPlatformOptions.TestModulesFilterOption))
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.TestModulesFilterOption))
count++;

if (parseResult.HasOption(TestingPlatformOptions.SolutionOption))
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.SolutionOption))
count++;

if (parseResult.HasOption(TestingPlatformOptions.ProjectOption))
if (parseResult.HasOption(MicrosoftTestingPlatformOptions.ProjectOption))
count++;

if (count > 1)
Expand All @@ -35,14 +35,14 @@ static void ValidatePathOptions(ParseResult parseResult)

static void ValidateOptionsIrrelevantToModulesFilter(ParseResult parseResult)
{
if (!parseResult.HasOption(TestingPlatformOptions.TestModulesFilterOption))
if (!parseResult.HasOption(MicrosoftTestingPlatformOptions.TestModulesFilterOption))
{
return;
}

if (parseResult.HasOption(CommonOptions.ArchitectureOption) ||
parseResult.HasOption(TestingPlatformOptions.ConfigurationOption) ||
parseResult.HasOption(TestingPlatformOptions.FrameworkOption) ||
parseResult.HasOption(MicrosoftTestingPlatformOptions.ConfigurationOption) ||
parseResult.HasOption(MicrosoftTestingPlatformOptions.FrameworkOption) ||
parseResult.HasOption(CommonOptions.OperatingSystemOption) ||
parseResult.HasOption(CommonOptions.RuntimeOptionName))
{
Expand Down
Loading
Loading