Skip to content

Commit 5a684aa

Browse files
authored
Cleanup dotnet test (#49734)
2 parents d77a1ee + c5752ff commit 5a684aa

File tree

7 files changed

+55
-65
lines changed

7 files changed

+55
-65
lines changed

src/Cli/dotnet/Commands/Run/RunCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ static void ValidatePreconditions(ProjectInstance project)
364364

365365
static RunProperties ReadRunPropertiesFromProject(ProjectInstance project, string[] applicationArgs)
366366
{
367-
var runProperties = RunProperties.FromProjectAndApplicationArguments(project, applicationArgs, fallbackToTargetPath: false);
367+
var runProperties = RunProperties.FromProjectAndApplicationArguments(project, applicationArgs);
368368
if (string.IsNullOrEmpty(runProperties.RunCommand))
369369
{
370370
ThrowUnableToRunError(project);

src/Cli/dotnet/Commands/Run/RunProperties.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,9 @@ namespace Microsoft.DotNet.Cli.Commands.Run;
88

99
internal record RunProperties(string RunCommand, string? RunArguments, string? RunWorkingDirectory)
1010
{
11-
internal static RunProperties FromProjectAndApplicationArguments(ProjectInstance project, string[] applicationArgs, bool fallbackToTargetPath)
11+
internal static RunProperties FromProjectAndApplicationArguments(ProjectInstance project, string[] applicationArgs)
1212
{
1313
string runProgram = project.GetPropertyValue("RunCommand");
14-
if (fallbackToTargetPath &&
15-
(string.IsNullOrEmpty(runProgram) || !File.Exists(runProgram)))
16-
{
17-
// If we can't find the executable that runCommand is pointing to, we simply use TargetPath instead.
18-
// In this case, we discard everything related to "Run" (i.e, RunWorkingDirectory and RunArguments) and use only TargetPath
19-
runProgram = project.GetPropertyValue("TargetPath");
20-
return new(runProgram, null, null);
21-
}
22-
2314
string runArguments = project.GetPropertyValue("RunArguments");
2415
string runWorkingDirectory = project.GetPropertyValue("RunWorkingDirectory");
2516

src/Cli/dotnet/Commands/Test/Models.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ public bool MoveNext()
111111
}
112112
}
113113

114-
internal sealed record TestModule(RunProperties RunProperties, string? ProjectFullPath, string? TargetFramework, bool IsTestingPlatformApplication, bool IsTestProject, ProjectLaunchSettingsModel? LaunchSettings);
114+
115+
internal sealed record TestModule(RunProperties RunProperties, string? ProjectFullPath, string? TargetFramework, bool IsTestingPlatformApplication, bool IsTestProject, ProjectLaunchSettingsModel? LaunchSettings, string TargetPath);
115116

116117
internal sealed record Handshake(Dictionary<byte, string>? Properties);
117118

src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,27 @@ public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModule
230230

231231
string targetFramework = project.GetPropertyValue(ProjectProperties.TargetFramework);
232232
RunProperties runProperties = GetRunProperties(project, loggers);
233+
234+
// dotnet run throws the same if RunCommand is null or empty.
235+
// In dotnet test, we are additionally checking that RunCommand is not dll.
236+
// In any "default" scenario, RunCommand is never dll.
237+
// If we found it to be dll, that is user explicitly setting RunCommand incorrectly.
238+
if (string.IsNullOrEmpty(runProperties.RunCommand) || runProperties.RunCommand.HasExtension(CliConstants.DLLExtension))
239+
{
240+
throw new GracefulException(
241+
string.Format(
242+
CliCommandStrings.RunCommandExceptionUnableToRun,
243+
"dotnet test",
244+
"OutputType",
245+
project.GetPropertyValue("OutputType")));
246+
}
247+
233248
string projectFullPath = project.GetPropertyValue(ProjectProperties.ProjectFullPath);
234249

235250
// TODO: Support --launch-profile and pass it here.
236251
var launchSettings = TryGetLaunchProfileSettings(Path.GetDirectoryName(projectFullPath)!, project.GetPropertyValue(ProjectProperties.AppDesignerFolder), noLaunchProfile, profileName: null);
237252

238-
return new TestModule(runProperties, PathUtility.FixFilePath(projectFullPath), targetFramework, isTestingPlatformApplication, isTestProject, launchSettings);
253+
return new TestModule(runProperties, PathUtility.FixFilePath(projectFullPath), targetFramework, isTestingPlatformApplication, isTestProject, launchSettings, project.GetPropertyValue(ProjectProperties.TargetPath));
239254

240255
static RunProperties GetRunProperties(ProjectInstance project, ICollection<ILogger>? loggers)
241256
{
@@ -247,12 +262,11 @@ static RunProperties GetRunProperties(ProjectInstance project, ICollection<ILogg
247262
{
248263
if (!project.Build(s_computeRunArgumentsTarget, loggers: null))
249264
{
250-
Logger.LogTrace(() => $"The target {s_computeRunArgumentsTarget} failed to build. Falling back to TargetPath.");
251-
return new RunProperties(project.GetPropertyValue(ProjectProperties.TargetPath), null, null);
265+
throw new GracefulException(CliCommandStrings.RunCommandEvaluationExceptionBuildFailed, s_computeRunArgumentsTarget);
252266
}
253267
}
254268

255-
return RunProperties.FromProjectAndApplicationArguments(project, [], fallbackToTargetPath: true);
269+
return RunProperties.FromProjectAndApplicationArguments(project, []);
256270
}
257271
}
258272

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

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using Microsoft.DotNet.Cli.Commands.Test.IPC;
99
using Microsoft.DotNet.Cli.Commands.Test.IPC.Models;
1010
using Microsoft.DotNet.Cli.Commands.Test.IPC.Serializers;
11-
using Microsoft.DotNet.Cli.Utils;
1211

1312
namespace Microsoft.DotNet.Cli.Commands.Test;
1413

@@ -54,12 +53,13 @@ public async Task<int> RunAsync(TestOptions testOptions)
5453

5554
private ProcessStartInfo CreateProcessStartInfo(TestOptions testOptions)
5655
{
57-
bool isDll = Module.RunProperties.RunCommand.HasExtension(CliConstants.DLLExtension);
58-
5956
var processStartInfo = new ProcessStartInfo
6057
{
61-
FileName = GetFileName(testOptions, isDll),
62-
Arguments = GetArguments(testOptions, isDll),
58+
// We should get correct RunProperties right away.
59+
// For the case of dotnet test --test-modules path/to/dll, the TestModulesFilterHandler is responsible
60+
// for providing the dotnet muxer as RunCommand, and `exec "path/to/dll"` as RunArguments.
61+
FileName = Module.RunProperties.RunCommand,
62+
Arguments = GetArguments(testOptions),
6363
RedirectStandardOutput = true,
6464
RedirectStandardError = true
6565
};
@@ -87,23 +87,27 @@ private ProcessStartInfo CreateProcessStartInfo(TestOptions testOptions)
8787
return processStartInfo;
8888
}
8989

90-
private string GetFileName(TestOptions testOptions, bool isDll)
91-
=> isDll ? Environment.ProcessPath : Module.RunProperties.RunCommand;
92-
93-
private string GetArguments(TestOptions testOptions, bool isDll)
90+
private string GetArguments(TestOptions testOptions)
9491
{
95-
if (testOptions.HasFilterMode || !isDll || !IsArchitectureSpecified(testOptions))
92+
// Keep RunArguments first.
93+
// In the case of UseAppHost=false, RunArguments is set to `exec $(TargetPath)`:
94+
// https://github.com/dotnet/sdk/blob/333388c31d811701e3b6be74b5434359151424dc/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets#L1411
95+
// So, we keep that first always.
96+
StringBuilder builder = new(Module.RunProperties.RunArguments);
97+
98+
if (testOptions.IsHelp)
9699
{
97-
return BuildArgs(testOptions, isDll);
100+
builder.Append($" {TestingPlatformOptions.HelpOption.Name} ");
98101
}
99102

100-
// If we reach here, that means we have a test project that doesn't produce an executable.
101-
throw new InvalidOperationException($"A Microsoft.Testing.Platform test project should produce an executable. The file '{Module.RunProperties.RunCommand}' is dll.");
102-
}
103+
var args = _buildOptions.UnmatchedTokens;
104+
builder.Append(args.Count != 0
105+
? args.Aggregate((a, b) => $"{a} {b}")
106+
: string.Empty);
103107

104-
private static bool IsArchitectureSpecified(TestOptions testOptions)
105-
{
106-
return !string.IsNullOrEmpty(testOptions.Architecture);
108+
builder.Append($" {CliConstants.ServerOptionKey} {CliConstants.ServerOptionValue} {CliConstants.DotNetTestPipeOptionKey} {_pipeNameDescription.Name}");
109+
110+
return builder.ToString();
107111
}
108112

109113
private void WaitOnTestApplicationPipeConnectionLoop()
@@ -277,35 +281,6 @@ private bool ModulePathExists()
277281
return true;
278282
}
279283

280-
private string BuildArgs(TestOptions testOptions, bool isDll)
281-
{
282-
StringBuilder builder = new();
283-
284-
if (isDll)
285-
{
286-
builder.Append($"exec {Module.RunProperties.RunCommand} ");
287-
}
288-
289-
AppendCommonArgs(builder, testOptions);
290-
291-
return builder.ToString();
292-
}
293-
294-
private void AppendCommonArgs(StringBuilder builder, TestOptions testOptions)
295-
{
296-
if (testOptions.IsHelp)
297-
{
298-
builder.Append($" {TestingPlatformOptions.HelpOption.Name} ");
299-
}
300-
301-
var args = _buildOptions.UnmatchedTokens;
302-
builder.Append(args.Count != 0
303-
? args.Aggregate((a, b) => $"{a} {b}")
304-
: string.Empty);
305-
306-
builder.Append($" {CliConstants.ServerOptionKey} {CliConstants.ServerOptionValue} {CliConstants.DotNetTestPipeOptionKey} {_pipeNameDescription.Name} {Module.RunProperties.RunArguments}");
307-
}
308-
309284
public void OnHandshakeMessage(HandshakeMessage handshakeMessage)
310285
{
311286
HandshakeReceived?.Invoke(this, new HandshakeArgs { Handshake = new Handshake(handshakeMessage.Properties) });

src/Cli/dotnet/Commands/Test/TestApplicationEventHandlers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public void OnHandshakeReceived(object sender, HandshakeArgs args)
2626
var executionId = args.Handshake.Properties[HandshakeMessagePropertyNames.ExecutionId];
2727
var arch = args.Handshake.Properties[HandshakeMessagePropertyNames.Architecture]?.ToLower();
2828
var tfm = TargetFrameworkParser.GetShortTargetFramework(args.Handshake.Properties[HandshakeMessagePropertyNames.Framework]);
29-
(string ModulePath, string TargetFramework, string Architecture, string ExecutionId) appInfo = new(testApplication.Module.RunProperties.RunCommand, tfm, arch, executionId);
29+
(string ModulePath, string TargetFramework, string Architecture, string ExecutionId) appInfo = new(testApplication.Module.TargetPath, tfm, arch, executionId);
3030
_executions[testApplication] = appInfo;
3131
_output.AssemblyRunStarted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId);
3232
}
@@ -147,7 +147,7 @@ public void OnTestProcessExited(object sender, TestProcessExitEventArgs args)
147147
}
148148
else
149149
{
150-
_output.AssemblyRunCompleted(testApplication.Module.RunProperties.RunCommand ?? testApplication.Module.ProjectFullPath, testApplication.Module.TargetFramework, architecture: null, null, args.ExitCode, string.Join(Environment.NewLine, args.OutputData), string.Join(Environment.NewLine, args.ErrorData));
150+
_output.AssemblyRunCompleted(testApplication.Module.TargetPath ?? testApplication.Module.ProjectFullPath, testApplication.Module.TargetFramework, architecture: null, null, args.ExitCode, string.Join(Environment.NewLine, args.OutputData), string.Join(Environment.NewLine, args.ErrorData));
151151
}
152152

153153
LogTestProcessExit(args);

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.DotNet.Cli.Commands.Run;
88
using Microsoft.DotNet.Cli.Commands.Test.Terminal;
99
using Microsoft.DotNet.Cli.Extensions;
10+
using Microsoft.DotNet.Cli.Utils;
1011
using Microsoft.Extensions.FileSystemGlobbing;
1112

1213
namespace Microsoft.DotNet.Cli.Commands.Test;
@@ -47,9 +48,17 @@ public bool RunWithTestModulesFilter(ParseResult parseResult)
4748
return false;
4849
}
4950

51+
var muxerPath = new Muxer().MuxerPath;
5052
foreach (string testModule in testModulePaths)
5153
{
52-
var testApp = new ParallelizableTestModuleGroupWithSequentialInnerModules(new TestModule(new RunProperties(testModule, null, null), null, null, true, true, null));
54+
// We want to produce the right RunCommand and RunArguments for TestApplication implementation to consume directly.
55+
// We don't want TestApplication class to be concerned about whether it's running dll via test module or not.
56+
// If we are given dll, we use dotnet exec. Otherwise, we run the executable directly.
57+
RunProperties runProperties = testModule.HasExtension(CliConstants.DLLExtension)
58+
? new RunProperties(muxerPath, $@"exec ""{testModule}""", null)
59+
: new RunProperties(testModule, null, null);
60+
61+
var testApp = new ParallelizableTestModuleGroupWithSequentialInnerModules(new TestModule(runProperties, null, null, true, true, null, testModule));
5362
// Write the test application to the channel
5463
_actionQueue.Enqueue(testApp);
5564
}

0 commit comments

Comments
 (0)