Skip to content

Commit 818246b

Browse files
mariam-abdullaEvangelinkViktorHofer
authored
Support --solution and --directory options in dotnet test (#45859)
Co-authored-by: Amaury Levé <[email protected]> Co-authored-by: Viktor Hofer <[email protected]>
1 parent 08924bc commit 818246b

24 files changed

+745
-283
lines changed

src/Cli/dotnet/commands/dotnet-test/BuiltInOptions.cs

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/Cli/dotnet/commands/dotnet-test/CliConstants.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,28 @@ internal static class CliConstants
1313

1414
public const string ServerOptionValue = "dotnettestcli";
1515

16-
public const string MSBuildExeName = "MSBuild.dll";
1716
public const string ParametersSeparator = "--";
17+
public const string SemiColon = ";";
18+
public const string Colon = ":";
1819

1920
public const string VSTest = "VSTest";
2021
public const string MicrosoftTestingPlatform = "MicrosoftTestingPlatform";
2122

2223
public const string TestSectionKey = "test";
2324

2425
public const string RestoreCommand = "restore";
26+
27+
public static readonly string[] ProjectExtensions = { ".proj", ".csproj", ".vbproj", ".fsproj" };
28+
public static readonly string[] SolutionExtensions = { ".sln", ".slnx" };
29+
30+
public const string ProjectExtensionPattern = "*.*proj";
31+
public const string SolutionExtensionPattern = "*.sln";
32+
public const string SolutionXExtensionPattern = "*.slnx";
33+
34+
public const string BinLogFileName = "msbuild.binlog";
35+
36+
public const string TestingPlatformVsTestBridgeRunSettingsFileEnvVar = "TESTINGPLATFORM_VSTESTBRIDGE_RUNSETTINGS_FILE";
37+
public const string DLLExtension = "dll";
2538
}
2639

2740
internal static class TestStates

src/Cli/dotnet/commands/dotnet-test/LocalizableStrings.resx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@
187187
<data name="CmdProjectDescription" xml:space="preserve">
188188
<value>Defines the path of the project file to run (folder name or full path). If not specified, it defaults to the current directory.</value>
189189
</data>
190+
<data name="CmdSolutionDescription" xml:space="preserve">
191+
<value>Defines the path of the solution file to run. If not specified, it defaults to the current directory.</value>
192+
</data>
193+
<data name="CmdDirectoryDescription" xml:space="preserve">
194+
<value>Defines the path of directory to run. If not specified, it defaults to the current directory.</value>
195+
</data>
190196
<data name="CmdResultsDirectoryDescription" xml:space="preserve">
191197
<value>The directory where the test results will be placed.
192198
The specified directory will be created if it does not exist.</value>
@@ -329,16 +335,28 @@ Examples:
329335
<data name="CmdUnsupportedTestRunnerDescription" xml:space="preserve">
330336
<value>Test runner not supported: {0}.</value>
331337
</data>
332-
<data name="CmdNonExistentProjectFilePathDescription" xml:space="preserve">
333-
<value>The provided project file path does not exist: {0}.</value>
338+
<data name="CmdNonExistentFileErrorDescription" xml:space="preserve">
339+
<value>The provided file path does not exist: {0}.</value>
340+
</data>
341+
<data name="CmdNonExistentDirectoryErrorDescription" xml:space="preserve">
342+
<value>The provided directory path does not exist: {0}.</value>
334343
</data>
335-
<data name="CmdMultipleProjectOrSolutionFilesErrorMessage" xml:space="preserve">
344+
<data name="CmdMultipleProjectOrSolutionFilesErrorDescription" xml:space="preserve">
336345
<value>Specify which project or solution file to use because this folder contains more than one project or solution file.</value>
337346
</data>
338-
<data name="CmdNoProjectOrSolutionFileErrorMessage" xml:space="preserve">
347+
<data name="CmdNoProjectOrSolutionFileErrorDescription" xml:space="preserve">
339348
<value>Specify a project or solution file. The current working directory does not contain a project or solution file.</value>
340349
</data>
341-
<data name="CmdMSBuildProjectsPropertiesErrorMessage" xml:space="preserve">
350+
<data name="CmdMSBuildProjectsPropertiesErrorDescription" xml:space="preserve">
342351
<value>Get projects properties with MSBuild didn't execute properly with exit code: {0}.</value>
343352
</data>
353+
<data name="CmdInvalidSolutionFileExtensionErrorDescription" xml:space="preserve">
354+
<value>The provided solution file has an invalid extension: {0}.</value>
355+
</data>
356+
<data name="CmdInvalidProjectFileExtensionErrorDescription" xml:space="preserve">
357+
<value>The provided project file has an invalid extension: {0}.</value>
358+
</data>
359+
<data name="CmdMultipleBuildPathOptionsErrorDescription" xml:space="preserve">
360+
<value>Specify either the project, solution or directory option.</value>
361+
</data>
344362
</root>

src/Cli/dotnet/commands/dotnet-test/MSBuildHandler.cs

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.Build.Execution;
77
using Microsoft.Build.Framework;
88
using Microsoft.Build.Logging;
9+
using Microsoft.DotNet.Tools.Test;
910

1011
namespace Microsoft.DotNet.Cli
1112
{
@@ -18,8 +19,6 @@ internal sealed class MSBuildHandler : IDisposable
1819
private readonly ConcurrentBag<TestApplication> _testApplications = new();
1920
private bool _areTestingPlatformApplications = true;
2021

21-
private const string BinLogFileName = "msbuild.binlog";
22-
private const string Separator = ";";
2322
private static readonly Lock buildLock = new();
2423

2524
public MSBuildHandler(List<string> args, TestApplicationActionQueue actionQueue, int degreeOfParallelism)
@@ -29,9 +28,86 @@ public MSBuildHandler(List<string> args, TestApplicationActionQueue actionQueue,
2928
_degreeOfParallelism = degreeOfParallelism;
3029
}
3130

32-
public async Task<int> RunWithMSBuild()
31+
public async Task<bool> RunMSBuild(BuildPathsOptions buildPathOptions)
3332
{
34-
bool solutionOrProjectFileFound = SolutionAndProjectUtility.TryGetProjectOrSolutionFilePath(Directory.GetCurrentDirectory(), out string projectOrSolutionFilePath, out bool isSolution);
33+
if (!ValidateBuildPathOptions(buildPathOptions))
34+
{
35+
return false;
36+
}
37+
38+
int msbuildExitCode;
39+
40+
if (!string.IsNullOrEmpty(buildPathOptions.ProjectPath))
41+
{
42+
msbuildExitCode = await RunBuild(buildPathOptions.ProjectPath, isSolution: false);
43+
}
44+
else if (!string.IsNullOrEmpty(buildPathOptions.SolutionPath))
45+
{
46+
msbuildExitCode = await RunBuild(buildPathOptions.SolutionPath, isSolution: true);
47+
}
48+
else
49+
{
50+
msbuildExitCode = await RunBuild(buildPathOptions.DirectoryPath ?? Directory.GetCurrentDirectory());
51+
}
52+
53+
if (msbuildExitCode != ExitCodes.Success)
54+
{
55+
VSTestTrace.SafeWriteTrace(() => string.Format(LocalizableStrings.CmdMSBuildProjectsPropertiesErrorDescription, msbuildExitCode));
56+
return false;
57+
}
58+
59+
return true;
60+
}
61+
62+
private bool ValidateBuildPathOptions(BuildPathsOptions buildPathOptions)
63+
{
64+
if ((!string.IsNullOrEmpty(buildPathOptions.ProjectPath) && !string.IsNullOrEmpty(buildPathOptions.SolutionPath)) ||
65+
(!string.IsNullOrEmpty(buildPathOptions.ProjectPath) && !string.IsNullOrEmpty(buildPathOptions.DirectoryPath)) ||
66+
(!string.IsNullOrEmpty(buildPathOptions.SolutionPath) && !string.IsNullOrEmpty(buildPathOptions.DirectoryPath)))
67+
{
68+
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdMultipleBuildPathOptionsErrorDescription);
69+
return false;
70+
}
71+
72+
if (!string.IsNullOrEmpty(buildPathOptions.ProjectPath))
73+
{
74+
return ValidateFilePath(buildPathOptions.ProjectPath, CliConstants.ProjectExtensions, LocalizableStrings.CmdInvalidProjectFileExtensionErrorDescription);
75+
}
76+
77+
if (!string.IsNullOrEmpty(buildPathOptions.SolutionPath))
78+
{
79+
return ValidateFilePath(buildPathOptions.SolutionPath, CliConstants.SolutionExtensions, LocalizableStrings.CmdInvalidSolutionFileExtensionErrorDescription);
80+
}
81+
82+
if (!string.IsNullOrEmpty(buildPathOptions.DirectoryPath) && !Directory.Exists(buildPathOptions.DirectoryPath))
83+
{
84+
VSTestTrace.SafeWriteTrace(() => string.Format(LocalizableStrings.CmdNonExistentDirectoryErrorDescription, Path.GetFullPath(buildPathOptions.DirectoryPath)));
85+
return false;
86+
}
87+
88+
return true;
89+
}
90+
91+
private static bool ValidateFilePath(string filePath, string[] validExtensions, string errorMessage)
92+
{
93+
if (!validExtensions.Contains(Path.GetExtension(filePath)))
94+
{
95+
VSTestTrace.SafeWriteTrace(() => string.Format(errorMessage, filePath));
96+
return false;
97+
}
98+
99+
if (!File.Exists(filePath))
100+
{
101+
VSTestTrace.SafeWriteTrace(() => string.Format(LocalizableStrings.CmdNonExistentFileErrorDescription, Path.GetFullPath(filePath)));
102+
return false;
103+
}
104+
105+
return true;
106+
}
107+
108+
private async Task<int> RunBuild(string directoryPath)
109+
{
110+
bool solutionOrProjectFileFound = SolutionAndProjectUtility.TryGetProjectOrSolutionFilePath(directoryPath, out string projectOrSolutionFilePath, out bool isSolution);
35111

36112
if (!solutionOrProjectFileFound)
37113
{
@@ -45,9 +121,9 @@ public async Task<int> RunWithMSBuild()
45121
return restored ? ExitCodes.Success : ExitCodes.GenericFailure;
46122
}
47123

48-
public async Task<int> RunWithMSBuild(string filePath)
124+
private async Task<int> RunBuild(string filePath, bool isSolution)
49125
{
50-
(IEnumerable<Module> modules, bool restored) = await GetProjectsProperties(filePath, false);
126+
(IEnumerable<Module> modules, bool restored) = await GetProjectsProperties(filePath, isSolution);
51127

52128
InitializeTestApplications(modules);
53129

@@ -92,7 +168,12 @@ public bool EnqueueTestApplications()
92168

93169
if (isSolution)
94170
{
95-
var projects = await SolutionAndProjectUtility.ParseSolution(solutionOrProjectFilePath);
171+
string fileDirectory = Path.GetDirectoryName(solutionOrProjectFilePath);
172+
string rootDirectory = string.IsNullOrEmpty(fileDirectory)
173+
? Directory.GetCurrentDirectory()
174+
: fileDirectory;
175+
176+
var projects = await SolutionAndProjectUtility.ParseSolution(solutionOrProjectFilePath, rootDirectory);
96177
ProcessProjectsInParallel(projects, allProjects, ref restored);
97178
}
98179
else
@@ -178,7 +259,7 @@ private static IEnumerable<Module> ExtractModulesFromProject(Project project)
178259
}
179260
else
180261
{
181-
var frameworks = targetFrameworks.Split(Separator, StringSplitOptions.RemoveEmptyEntries);
262+
var frameworks = targetFrameworks.Split(CliConstants.SemiColon, StringSplitOptions.RemoveEmptyEntries);
182263
foreach (var framework in frameworks)
183264
{
184265
project.SetProperty(ProjectProperties.TargetFramework, framework);
@@ -225,8 +306,7 @@ private static BuildResult RestoreProject(string projectFilePath, ProjectCollect
225306

226307
private static bool IsBinaryLoggerEnabled(List<string> args, out string binLogFileName)
227308
{
228-
binLogFileName = BinLogFileName;
229-
309+
binLogFileName = string.Empty;
230310
var binLogArgs = new List<string>();
231311

232312
foreach (var arg in args)
@@ -248,10 +328,16 @@ private static bool IsBinaryLoggerEnabled(List<string> args, out string binLogFi
248328
// Get BinLog filename
249329
var binLogArg = binLogArgs.LastOrDefault();
250330

251-
if (binLogArg.Contains(':'))
331+
if (binLogArg.Contains(CliConstants.Colon))
252332
{
253-
binLogFileName = binLogArg.Split(':')[1];
333+
var parts = binLogArg.Split(CliConstants.Colon, 2);
334+
binLogFileName = !string.IsNullOrEmpty(parts[1]) ? parts[1] : CliConstants.BinLogFileName;
254335
}
336+
else
337+
{
338+
binLogFileName = CliConstants.BinLogFileName;
339+
}
340+
255341
return true;
256342
}
257343

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.DotNet.Cli
5+
{
6+
internal record BuildConfigurationOptions(bool HasNoRestore, bool HasNoBuild, string Configuration, string Architecture);
7+
8+
internal record BuildPathsOptions(string ProjectPath, string SolutionPath, string DirectoryPath);
9+
}

src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,80 +21,63 @@ public static bool TryGetProjectOrSolutionFilePath(string directory, out string
2121
return false;
2222
}
2323

24-
string[] possibleSolutionPaths = [
25-
..Directory.GetFiles(directory, "*.sln", SearchOption.TopDirectoryOnly),
26-
..Directory.GetFiles(directory, "*.slnx", SearchOption.TopDirectoryOnly)];
24+
var possibleSolutionPaths = GetSolutionFilePaths(directory);
2725

28-
// If more than a single sln file is found, an error is thrown since we can't determine which one to choose.
29-
if (possibleSolutionPaths.Length > 1)
30-
{
31-
VSTestTrace.SafeWriteTrace(() => string.Format(CommonLocalizableStrings.MoreThanOneSolutionInDirectory, directory));
32-
return false;
33-
}
34-
// If a single solution is found, use it.
35-
else if (possibleSolutionPaths.Length == 1)
36-
{
37-
// Get project file paths to check if there are any projects in the directory
38-
string[] possibleProjectPaths = GetProjectFilePaths(directory);
26+
// If more than a single sln file is found, an error is thrown since we can't determine which one to choose.
27+
if (possibleSolutionPaths.Length > 1)
28+
{
29+
VSTestTrace.SafeWriteTrace(() => string.Format(CommonLocalizableStrings.MoreThanOneSolutionInDirectory, directory));
30+
return false;
31+
}
32+
33+
if (possibleSolutionPaths.Length == 1)
34+
{
35+
var possibleProjectPaths = GetProjectFilePaths(directory);
3936

4037
if (possibleProjectPaths.Length == 0)
4138
{
4239
projectOrSolutionFilePath = possibleSolutionPaths[0];
4340
isSolution = true;
4441
return true;
4542
}
46-
else // If both solution and project files are found, return false
47-
{
48-
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdMultipleProjectOrSolutionFilesErrorMessage);
49-
return false;
50-
}
43+
44+
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdMultipleProjectOrSolutionFilesErrorDescription);
45+
return false;
5146
}
52-
// If no solutions are found, look for a project file
53-
else
47+
else // If no solutions are found, look for a project file
5448
{
5549
string[] possibleProjectPath = GetProjectFilePaths(directory);
5650

57-
// No projects found throws an error that no sln nor projects were found
5851
if (possibleProjectPath.Length == 0)
5952
{
60-
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdNoProjectOrSolutionFileErrorMessage);
53+
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdNoProjectOrSolutionFileErrorDescription);
6154
return false;
6255
}
63-
// A single project found, use it
64-
else if (possibleProjectPath.Length == 1)
56+
57+
if (possibleProjectPath.Length == 1)
6558
{
6659
projectOrSolutionFilePath = possibleProjectPath[0];
6760
return true;
6861
}
69-
// More than one project found. Not sure which one to choose
70-
else
71-
{
72-
VSTestTrace.SafeWriteTrace(() => string.Format(CommonLocalizableStrings.MoreThanOneProjectInDirectory, directory));
73-
return false;
74-
}
62+
63+
VSTestTrace.SafeWriteTrace(() => string.Format(CommonLocalizableStrings.MoreThanOneProjectInDirectory, directory));
64+
65+
return false;
7566
}
7667
}
7768

78-
79-
private static string[] GetProjectFilePaths(string directory)
69+
private static string[] GetSolutionFilePaths(string directory)
8070
{
81-
var projectFiles = Directory.EnumerateFiles(directory, "*.*proj", SearchOption.TopDirectoryOnly)
82-
.Where(IsProjectFile)
71+
return Directory.EnumerateFiles(directory, CliConstants.SolutionExtensionPattern, SearchOption.TopDirectoryOnly)
72+
.Concat(Directory.EnumerateFiles(directory, CliConstants.SolutionXExtensionPattern, SearchOption.TopDirectoryOnly))
8373
.ToArray();
84-
85-
return projectFiles;
8674
}
8775

88-
private static bool IsProjectFile(string filePath)
89-
{
90-
var extension = Path.GetExtension(filePath);
91-
return extension.Equals(".csproj", StringComparison.OrdinalIgnoreCase) ||
92-
extension.Equals(".vbproj", StringComparison.OrdinalIgnoreCase) ||
93-
extension.Equals(".fsproj", StringComparison.OrdinalIgnoreCase) ||
94-
extension.Equals(".proj", StringComparison.OrdinalIgnoreCase);
95-
}
76+
private static string[] GetProjectFilePaths(string directory) => [.. Directory.EnumerateFiles(directory, CliConstants.ProjectExtensionPattern, SearchOption.TopDirectoryOnly).Where(IsProjectFile)];
77+
78+
private static bool IsProjectFile(string filePath) => CliConstants.ProjectExtensions.Contains(Path.GetExtension(filePath), StringComparer.OrdinalIgnoreCase);
9679

97-
public static async Task<IEnumerable<string>> ParseSolution(string solutionFilePath)
80+
public static async Task<IEnumerable<string>> ParseSolution(string solutionFilePath, string directory)
9881
{
9982
if (string.IsNullOrEmpty(solutionFilePath))
10083
{
@@ -119,7 +102,7 @@ public static async Task<IEnumerable<string>> ParseSolution(string solutionFileP
119102

120103
if (solution is not null)
121104
{
122-
projectsPaths = [.. solution.SolutionProjects.Select(project => Path.GetFullPath(project.FilePath))];
105+
projectsPaths.AddRange(solution.SolutionProjects.Select(project => Path.Combine(directory, project.FilePath)));
123106
}
124107

125108
return projectsPaths;

0 commit comments

Comments
 (0)