Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execute dotnet run behind the hood in dotnet test #43170

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Cli/dotnet/commands/dotnet-test/BuiltInOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.Cli
{
internal record BuiltInOptions(bool HasNoRestore, bool HasNoBuild, string Configuration, string Architecture);
}
8 changes: 4 additions & 4 deletions src/Cli/dotnet/commands/dotnet-test/CliConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ namespace Microsoft.DotNet.Cli
{
internal static class CliConstants
{
public const string DotnetRunCommand = "dotnet run";
public const string HelpOptionKey = "--help";
public const string MSBuildOptionKey = "--msbuild-params";
public const string NoBuildOptionKey = "--no-build";
public const string ServerOptionKey = "--server";
public const string DotNetTestPipeOptionKey = "--dotnet-test-pipe";
public const string DegreeOfParallelismOptionKey = "--degree-of-parallelism";
public const string DOPOptionKey = "--dop";
public const string ProjectOptionKey = "--project";
public const string FrameworkOptionKey = "--framework";

public const string ServerOptionValue = "dotnettestcli";

public const string MSBuildExeName = "MSBuild.dll";
public const string ParametersSeparator = "--";
}

internal static class TestStates
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/dotnet/commands/dotnet-test/IPC/Models/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

namespace Microsoft.DotNet.Tools.Test;

internal sealed record class Module(string? DLLPath, string? ProjectPath) : IRequest;
internal sealed record Module(string? DLLPath, string? ProjectPath, string? TargetFramework) : IRequest;
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ public object Deserialize(Stream stream)
{
string modulePath = ReadString(stream);
string projectPath = ReadString(stream);
return new Module(modulePath.Trim(), projectPath.Trim());
string targetFramework = ReadString(stream);
return new Module(modulePath.Trim(), projectPath.Trim(), targetFramework.Trim());
}

public void Serialize(object objectToSerialize, Stream stream)
{
WriteString(stream, ((Module)objectToSerialize).DLLPath);
WriteString(stream, ((Module)objectToSerialize).ProjectPath);
WriteString(stream, ((Module)objectToSerialize).TargetFramework);
}
}
}
3 changes: 3 additions & 0 deletions src/Cli/dotnet/commands/dotnet-test/LocalizableStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@
<data name="CmdArchitectureDescription" xml:space="preserve">
<value>The target architecture '{0}' on which tests will run.</value>
</data>
<data name="CmdConfigurationDescription" xml:space="preserve">
<value>Defines the build configuration. The default for most projects is Debug, but you can override the build configuration settings in your project.</value>
</data>
<data name="CmdResultsDirectoryDescription" xml:space="preserve">
<value>The directory where the test results will be placed.
The specified directory will be created if it does not exist.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private Task<IResponse> OnRequest(IRequest request)
throw new NotSupportedException($"Request '{request.GetType()}' is unsupported.");
}

var testApp = new TestApplication(module.DLLPath, _args);
var testApp = new TestApplication(module, _args);
// Write the test application to the channel
_actionQueue.Enqueue(testApp);
testApp.OnCreated();
Expand All @@ -82,12 +82,9 @@ private Task<IResponse> OnRequest(IRequest request)

public int RunWithMSBuild(ParseResult parseResult)
{
bool containsNoBuild = parseResult.HasOption(TestingPlatformOptions.NoBuildOption);
bool containsNoRestore = parseResult.HasOption(TestingPlatformOptions.NoRestoreOption) || containsNoBuild;

List<string> msbuildCommandLineArgs =
[
$"-t:{(containsNoRestore ? string.Empty : "Restore;")}{(containsNoBuild ? string.Empty : "Build;")}_GetTestsProject",
$"-t:_GetTestsProject",
$"-p:GetTestsProjectPipeName={_pipeNameDescription.Name}",
"-verbosity:q"
];
Expand Down
93 changes: 79 additions & 14 deletions src/Cli/dotnet/commands/dotnet-test/TestApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace Microsoft.DotNet.Cli
{
internal sealed class TestApplication : IDisposable
{
private readonly string _modulePath;
private readonly Module _module;

private readonly string[] _args;
private readonly List<string> _outputData = [];
private readonly List<string> _errorData = [];
Expand All @@ -33,11 +34,11 @@ internal sealed class TestApplication : IDisposable
public event EventHandler<EventArgs> Created;
public event EventHandler<ExecutionEventArgs> ExecutionIdReceived;

public string ModulePath => _modulePath;
public Module Module => _module;

public TestApplication(string modulePath, string[] args)
public TestApplication(Module module, string[] args)
{
_modulePath = modulePath;
_module = module;
_args = args;
}

Expand All @@ -46,20 +47,20 @@ public void AddExecutionId(string executionId)
_ = _executionIds.GetOrAdd(executionId, _ => string.Empty);
}

public async Task<int> RunAsync(bool enableHelp)
public async Task<int> RunAsync(bool isFilterMode, bool enableHelp, BuiltInOptions builtInOptions)
{
if (!ModulePathExists())
{
return 1;
}

bool isDll = _modulePath.EndsWith(".dll");
bool isDll = _module.DLLPath.EndsWith(".dll");
ProcessStartInfo processStartInfo = new()
{
FileName = isDll ?
Environment.ProcessPath :
_modulePath,
Arguments = enableHelp ? BuildHelpArgs(isDll) : BuildArgs(isDll),
_module.DLLPath,
Arguments = enableHelp ? BuildHelpArgs(isDll) : isFilterMode ? BuildArgs(isDll) : BuildArgsWithDotnetRun(builtInOptions),
RedirectStandardOutput = true,
RedirectStandardError = true
};
Expand All @@ -70,7 +71,6 @@ public async Task<int> RunAsync(bool enableHelp)
_namedPipeConnectionLoop.Wait();
return result;
}

private async Task WaitConnectionAsync(CancellationToken token)
{
try
Expand Down Expand Up @@ -223,21 +223,63 @@ private void StoreOutputAndErrorData(Process process)

private bool ModulePathExists()
{
if (!File.Exists(_modulePath))
if (!File.Exists(_module.DLLPath))
{
ErrorReceived.Invoke(this, new ErrorEventArgs { ErrorMessage = $"Test module '{_modulePath}' not found. Build the test application before or run 'dotnet test'." });
ErrorReceived.Invoke(this, new ErrorEventArgs { ErrorMessage = $"Test module '{_module.DLLPath}' not found. Build the test application before or run 'dotnet test'." });
return false;
}
return true;
}

private string BuildArgsWithDotnetRun(BuiltInOptions builtInOptions)
{
StringBuilder builder = new();

builder.Append($"{CliConstants.DotnetRunCommand} {CliConstants.ProjectOptionKey} \"{_module.ProjectPath}\"");

if (builtInOptions.HasNoRestore)
{
builder.Append($" {TestingPlatformOptions.NoRestoreOption.Name}");
}

if (builtInOptions.HasNoBuild)
{
builder.Append($" {TestingPlatformOptions.NoBuildOption.Name}");
}

if (!string.IsNullOrEmpty(builtInOptions.Architecture))
{
builder.Append($" {TestingPlatformOptions.ArchitectureOption.Name} {builtInOptions.Architecture}");
}

if (!string.IsNullOrEmpty(builtInOptions.Configuration))
{
builder.Append($" {TestingPlatformOptions.ConfigurationOption.Name} {builtInOptions.Configuration}");
}

if (!string.IsNullOrEmpty(_module.TargetFramework))
{
builder.Append($" {CliConstants.FrameworkOptionKey} {_module.TargetFramework}");
}

builder.Append($" {CliConstants.ParametersSeparator} ");

builder.Append(_args.Length != 0
? _args.Aggregate((a, b) => $"{a} {b}")
: string.Empty);

builder.Append($" {CliConstants.ServerOptionKey} {CliConstants.ServerOptionValue} {CliConstants.DotNetTestPipeOptionKey} {_pipeNameDescription.Name}");

return builder.ToString();
}

private string BuildArgs(bool isDll)
{
StringBuilder builder = new();

if (isDll)
{
builder.Append($"exec {_modulePath} ");
builder.Append($"exec {_module.DLLPath} ");
}

builder.Append(_args.Length != 0
Expand All @@ -255,7 +297,7 @@ private string BuildHelpArgs(bool isDll)

if (isDll)
{
builder.Append($"exec {_modulePath} ");
builder.Append($"exec {_module.DLLPath} ");
}

builder.Append($" {CliConstants.HelpOptionKey} {CliConstants.ServerOptionKey} {CliConstants.ServerOptionValue} {CliConstants.DotNetTestPipeOptionKey} {_pipeNameDescription.Name}");
Expand All @@ -267,7 +309,8 @@ public void OnHandshakeInfo(HandshakeInfo handshakeInfo)
{
if (handshakeInfo.Properties.TryGetValue(HandshakeInfoPropertyNames.ExecutionId, out string executionId))
{
ExecutionIdReceived?.Invoke(this, new ExecutionEventArgs { ModulePath = _modulePath, ExecutionId = executionId });
AddExecutionId(executionId);
ExecutionIdReceived?.Invoke(this, new ExecutionEventArgs { ModulePath = _module.DLLPath, ExecutionId = executionId });
}
HandshakeInfoReceived?.Invoke(this, new HandshakeInfoArgs { handshakeInfo = handshakeInfo });
}
Expand Down Expand Up @@ -307,6 +350,28 @@ internal void OnCreated()
Created?.Invoke(this, EventArgs.Empty);
}

public override string ToString()
{
StringBuilder builder = new();

if (!string.IsNullOrEmpty(_module.DLLPath))
{
builder.Append($"DLL: {_module.DLLPath}");
}

if (!string.IsNullOrEmpty(_module.ProjectPath))
{
builder.Append($"Project: {_module.ProjectPath}");
};

if (!string.IsNullOrEmpty(_module.TargetFramework))
{
builder.Append($"Target Framework: {_module.TargetFramework}");
};

return builder.ToString();
}

public void Dispose()
{
_pipeConnection?.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public TestApplicationActionQueue(int dop, Func<TestApplication, Task<int>> acti
public void Enqueue(TestApplication testApplication)
{
if (!_channel.Writer.TryWrite(testApplication))
throw new InvalidOperationException($"Failed to write to channel for test application: {testApplication.ModulePath}");
throw new InvalidOperationException($"Failed to write to channel for test application: {testApplication}");
mariam-abdulla marked this conversation as resolved.
Show resolved Hide resolved
}

public bool WaitAllActions()
Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet/commands/dotnet-test/TestCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ private static CliCommand GetTestingPlatformCliCommand()
command.Options.Add(TestingPlatformOptions.NoBuildOption);
command.Options.Add(TestingPlatformOptions.NoRestoreOption);
command.Options.Add(TestingPlatformOptions.ArchitectureOption);
command.Options.Add(TestingPlatformOptions.ConfigurationOption);

return command;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public bool RunWithTestModulesFilter(ParseResult parseResult)

foreach (string testModule in testModulePaths)
{
var testApp = new TestApplication(testModule, _args);
var testApp = new TestApplication(new Module(testModule, null, null), _args);
mariam-abdulla marked this conversation as resolved.
Show resolved Hide resolved
// Write the test application to the channel
_actionQueue.Enqueue(testApp);
testApp.OnCreated();
Expand Down
27 changes: 12 additions & 15 deletions src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.DotNet.Cli
{
internal partial class TestingPlatformCommand : CliCommand, ICustomHelp
{
private readonly ConcurrentDictionary<string, TestApplication> _testApplications = [];
private readonly ConcurrentBag<TestApplication> _testApplications = [];
private readonly CancellationTokenSource _cancellationToken = new();

private MSBuildConnectionHandler _msBuildConnectionHandler;
Expand All @@ -26,17 +26,18 @@ public TestingPlatformCommand(string name, string description = null) : base(nam

public int Run(ParseResult parseResult)
{
if (parseResult.HasOption(TestingPlatformOptions.ArchitectureOption))
{
VSTestTrace.SafeWriteTrace(() => $"The --arch option is not yet supported.");
return ExitCodes.GenericFailure;
}

// User can decide what the degree of parallelism should be
// If not specified, we will default to the number of processors
if (!int.TryParse(parseResult.GetValue(TestingPlatformOptions.MaxParallelTestModulesOption), out int degreeOfParallelism))
degreeOfParallelism = Environment.ProcessorCount;

bool filterModeEnabled = parseResult.HasOption(TestingPlatformOptions.TestModulesFilterOption);
BuiltInOptions builtInOptions = new(
parseResult.HasOption(TestingPlatformOptions.NoRestoreOption),
parseResult.HasOption(TestingPlatformOptions.NoBuildOption),
parseResult.GetValue(TestingPlatformOptions.ConfigurationOption),
parseResult.GetValue(TestingPlatformOptions.ArchitectureOption));

if (ContainsHelpOption(parseResult.GetArguments()))
{
_actionQueue = new(degreeOfParallelism, async (TestApplication testApp) =>
Expand All @@ -47,7 +48,7 @@ public int Run(ParseResult parseResult)
testApp.Created += OnTestApplicationCreated;
testApp.ExecutionIdReceived += OnExecutionIdReceived;

return await testApp.RunAsync(enableHelp: true);
return await testApp.RunAsync(filterModeEnabled, enableHelp: true, builtInOptions);
});
}
else
Expand All @@ -65,7 +66,7 @@ public int Run(ParseResult parseResult)
testApp.Created += OnTestApplicationCreated;
testApp.ExecutionIdReceived += OnExecutionIdReceived;

return await testApp.RunAsync(enableHelp: false);
return await testApp.RunAsync(filterModeEnabled, enableHelp: false, builtInOptions);
});
}

Expand Down Expand Up @@ -108,7 +109,7 @@ public int Run(ParseResult parseResult)
private void CleanUp()
{
_msBuildConnectionHandler.Dispose();
foreach (var testApplication in _testApplications.Values)
foreach (var testApplication in _testApplications)
{
testApplication.Dispose();
}
Expand Down Expand Up @@ -222,15 +223,11 @@ private void OnTestProcessExited(object sender, TestProcessExitEventArgs args)
private void OnTestApplicationCreated(object sender, EventArgs args)
{
TestApplication testApp = sender as TestApplication;
_testApplications[testApp.ModulePath] = testApp;
_testApplications.Add(testApp);
}

private void OnExecutionIdReceived(object sender, ExecutionEventArgs args)
{
if (_testApplications.TryGetValue(args.ModulePath, out var testApp))
{
testApp.AddExecutionId(args.ExecutionId);
}
}

private static bool ContainsHelpOption(IEnumerable<string> args) => args.Contains(CliConstants.HelpOptionKey) || args.Contains(CliConstants.HelpOptionKey.Substring(0, 2));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,11 @@ internal static class TestingPlatformOptions
Description = LocalizableStrings.CmdArchitectureDescription,
Arity = ArgumentArity.ExactlyOne
};

public static readonly CliOption<string> ConfigurationOption = new("--configuration")
{
Description = LocalizableStrings.CmdConfigurationDescription,
Arity = ArgumentArity.ExactlyOne
};
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading