Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e9c2a23
Use msbuild APIs to load projects
mariam-abdulla Jan 3, 2025
e7bcc8a
Add space
mariam-abdulla Jan 3, 2025
8d747c7
Fix formatting
mariam-abdulla Jan 6, 2025
2b02833
Add comments
mariam-abdulla Jan 6, 2025
f4cce4e
Update src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs
mariam-abdulla Jan 6, 2025
f078060
Replace 0 and 1 with constants
mariam-abdulla Jan 6, 2025
4c9add2
Merge branch 'dev/mabdullah/use-msbuild-apis-to-retrieve-project-prop…
mariam-abdulla Jan 6, 2025
771c1f1
Return from InitializeTestApplications() if _areTestingPlatformApplic…
mariam-abdulla Jan 6, 2025
def3cb8
Update method name
mariam-abdulla Jan 6, 2025
3b75ca8
Use aggregator overload in ProcessProjectsInParallel()
mariam-abdulla Jan 6, 2025
8e3b723
Apply some comments
mariam-abdulla Jan 6, 2025
b7b79ef
Remove ?
mariam-abdulla Jan 7, 2025
5349e6c
Update GetSolutionFilePaths
mariam-abdulla Jan 7, 2025
cbc3389
Use Microsoft.VisualStudio.SolutionPersistence namespace to parse sol…
mariam-abdulla Jan 7, 2025
9284893
Add GetSlnFileFullPath and GetProjectFileFullPath methods in Extensio…
mariam-abdulla Jan 7, 2025
6b42f48
Refactor
mariam-abdulla Jan 7, 2025
51c7424
Update ParseSolution to use GetSerializerByMoniker
mariam-abdulla Jan 7, 2025
3855845
Merge branch 'main' into dev/mabdullah/use-msbuild-apis-to-retrieve-p…
mariam-abdulla Jan 8, 2025
082f260
Update src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs
mariam-abdulla Jan 8, 2025
ae87351
Update src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs
mariam-abdulla Jan 8, 2025
3e26cd3
Update src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs
mariam-abdulla Jan 8, 2025
31593b0
Update src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs
mariam-abdulla Jan 8, 2025
f781164
Update src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs
mariam-abdulla Jan 8, 2025
7e359d1
Merge branch 'main' into dev/mabdullah/use-msbuild-apis-to-retrieve-p…
mariam-abdulla Jan 8, 2025
3c3d56b
Update src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs
mariam-abdulla Jan 8, 2025
55d247d
Apply comments
mariam-abdulla Jan 8, 2025
d3b11c5
merge
mariam-abdulla Jan 8, 2025
41323d4
Update src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs
mariam-abdulla Jan 8, 2025
a774524
Use Lock
mariam-abdulla Jan 8, 2025
9d17be2
Merge branch 'dev/mabdullah/use-msbuild-apis-to-retrieve-project-prop…
mariam-abdulla Jan 8, 2025
3f66a69
Update -bl logic
mariam-abdulla Jan 8, 2025
97b5ae9
Merge branch 'main' into dev/mabdullah/use-msbuild-apis-to-retrieve-p…
mariam-abdulla Jan 8, 2025
4f2b769
Remove unused var
mariam-abdulla Jan 8, 2025
6504a67
Merge branch 'dev/mabdullah/use-msbuild-apis-to-retrieve-project-prop…
mariam-abdulla Jan 8, 2025
c3dc4f2
Support --solution
mariam-abdulla Jan 9, 2025
d96f016
merge
mariam-abdulla Jan 9, 2025
517c20a
refactor code
mariam-abdulla Jan 9, 2025
5d05184
Fix bug
mariam-abdulla Jan 10, 2025
2341d1a
merge
mariam-abdulla Jan 10, 2025
3bf45f4
Remove @
mariam-abdulla Jan 10, 2025
223b072
Fixes
mariam-abdulla Jan 10, 2025
542560a
Merge branch 'main' into dev/mabdullah/support-solution-option-in-dot…
ViktorHofer Jan 10, 2025
efa4fb4
Apply comments
mariam-abdulla Jan 10, 2025
6765a26
Merge branch 'dev/mabdullah/support-solution-option-in-dotnet-test' o…
mariam-abdulla Jan 10, 2025
a36f471
Apply comments
mariam-abdulla Jan 10, 2025
47bced1
remove unused vars
mariam-abdulla Jan 10, 2025
9712e05
Support --directory
mariam-abdulla Jan 13, 2025
e1969ee
Merge branch 'main' into dev/mabdullah/support-solution-option-in-dot…
mariam-abdulla Jan 13, 2025
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: 0 additions & 7 deletions src/Cli/dotnet/commands/dotnet-test/BuiltInOptions.cs

This file was deleted.

15 changes: 14 additions & 1 deletion src/Cli/dotnet/commands/dotnet-test/CliConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,28 @@ internal static class CliConstants

public const string ServerOptionValue = "dotnettestcli";

public const string MSBuildExeName = "MSBuild.dll";
public const string ParametersSeparator = "--";
public const string SemiColon = ";";
public const string Colon = ":";

public const string VSTest = "VSTest";
public const string MicrosoftTestingPlatform = "MicrosoftTestingPlatform";

public const string TestSectionKey = "test";

public const string RestoreCommand = "restore";

public static readonly string[] ProjectExtensions = { ".proj", ".csproj", ".vbproj", ".fsproj" };
public static readonly string[] SolutionExtensions = { ".sln", ".slnx" };

public const string ProjectExtensionPattern = "*.*proj";
public const string SolutionExtensionPattern = "*.sln";
public const string SolutionXExtensionPattern = "*.slnx";

public const string BinLogFileName = "msbuild.binlog";

public const string TestingPlatformVsTestBridgeRunSettingsFileEnvVar = "TESTINGPLATFORM_VSTESTBRIDGE_RUNSETTINGS_FILE";
public const string DLLExtension = "dll";
}

internal static class TestStates
Expand Down
28 changes: 23 additions & 5 deletions src/Cli/dotnet/commands/dotnet-test/LocalizableStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@
<data name="CmdProjectDescription" xml:space="preserve">
<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>
</data>
<data name="CmdSolutionDescription" xml:space="preserve">
<value>Defines the path of the solution file to run. If not specified, it defaults to the current directory.</value>
</data>
<data name="CmdDirectoryDescription" xml:space="preserve">
<value>Defines the path of directory to run. If not specified, it defaults to the current directory.</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 Expand Up @@ -329,16 +335,28 @@ Examples:
<data name="CmdUnsupportedTestRunnerDescription" xml:space="preserve">
<value>Test runner not supported: {0}.</value>
</data>
<data name="CmdNonExistentProjectFilePathDescription" xml:space="preserve">
<value>The provided project file path does not exist: {0}.</value>
<data name="CmdNonExistentFileErrorDescription" xml:space="preserve">
<value>The provided file path does not exist: {0}.</value>
</data>
<data name="CmdNonExistentDirectoryErrorDescription" xml:space="preserve">
<value>The provided directory path does not exist: {0}.</value>
</data>
<data name="CmdMultipleProjectOrSolutionFilesErrorMessage" xml:space="preserve">
<data name="CmdMultipleProjectOrSolutionFilesErrorDescription" xml:space="preserve">
<value>Specify which project or solution file to use because this folder contains more than one project or solution file.</value>
</data>
<data name="CmdNoProjectOrSolutionFileErrorMessage" xml:space="preserve">
<data name="CmdNoProjectOrSolutionFileErrorDescription" xml:space="preserve">
<value>Specify a project or solution file. The current working directory does not contain a project or solution file.</value>
</data>
<data name="CmdMSBuildProjectsPropertiesErrorMessage" xml:space="preserve">
<data name="CmdMSBuildProjectsPropertiesErrorDescription" xml:space="preserve">
<value>Get projects properties with MSBuild didn't execute properly with exit code: {0}.</value>
</data>
<data name="CmdInvalidSolutionFileExtensionErrorDescription" xml:space="preserve">
<value>The provided solution file has an invalid extension: {0}.</value>
</data>
<data name="CmdInvalidProjectFileExtensionErrorDescription" xml:space="preserve">
<value>The provided project file has an invalid extension: {0}.</value>
</data>
<data name="CmdMultipleBuildPathOptionsErrorDescription" xml:space="preserve">
<value>Specify either the project, solution or directory option.</value>
</data>
</root>
110 changes: 98 additions & 12 deletions src/Cli/dotnet/commands/dotnet-test/MSBuildHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.DotNet.Tools.Test;

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

private const string BinLogFileName = "msbuild.binlog";
private const string Separator = ";";
private static readonly Lock buildLock = new();

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

public async Task<int> RunWithMSBuild()
public async Task<bool> RunMSBuild(BuildPathsOptions buildPathOptions)
{
bool solutionOrProjectFileFound = SolutionAndProjectUtility.TryGetProjectOrSolutionFilePath(Directory.GetCurrentDirectory(), out string projectOrSolutionFilePath, out bool isSolution);
if (!ValidateBuildPathOptions(buildPathOptions))
{
return false;
}

int msbuildExitCode;

if (!string.IsNullOrEmpty(buildPathOptions.ProjectPath))
{
msbuildExitCode = await RunBuild(buildPathOptions.ProjectPath, isSolution: false);
}
else if (!string.IsNullOrEmpty(buildPathOptions.SolutionPath))
{
msbuildExitCode = await RunBuild(buildPathOptions.SolutionPath, isSolution: true);
}
else
{
msbuildExitCode = await RunBuild(buildPathOptions.DirectoryPath ?? Directory.GetCurrentDirectory());
}

if (msbuildExitCode != ExitCodes.Success)
{
VSTestTrace.SafeWriteTrace(() => string.Format(LocalizableStrings.CmdMSBuildProjectsPropertiesErrorDescription, msbuildExitCode));
return false;
}

return true;
}

private bool ValidateBuildPathOptions(BuildPathsOptions buildPathOptions)
{
if ((!string.IsNullOrEmpty(buildPathOptions.ProjectPath) && !string.IsNullOrEmpty(buildPathOptions.SolutionPath)) ||
(!string.IsNullOrEmpty(buildPathOptions.ProjectPath) && !string.IsNullOrEmpty(buildPathOptions.DirectoryPath)) ||
(!string.IsNullOrEmpty(buildPathOptions.SolutionPath) && !string.IsNullOrEmpty(buildPathOptions.DirectoryPath)))
{
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdMultipleBuildPathOptionsErrorDescription);
return false;
}

if (!string.IsNullOrEmpty(buildPathOptions.ProjectPath))
{
return ValidateFilePath(buildPathOptions.ProjectPath, CliConstants.ProjectExtensions, LocalizableStrings.CmdInvalidProjectFileExtensionErrorDescription);
}

if (!string.IsNullOrEmpty(buildPathOptions.SolutionPath))
{
return ValidateFilePath(buildPathOptions.SolutionPath, CliConstants.SolutionExtensions, LocalizableStrings.CmdInvalidSolutionFileExtensionErrorDescription);
}

if (!string.IsNullOrEmpty(buildPathOptions.DirectoryPath) && !Directory.Exists(buildPathOptions.DirectoryPath))
{
VSTestTrace.SafeWriteTrace(() => string.Format(LocalizableStrings.CmdNonExistentDirectoryErrorDescription, Path.GetFullPath(buildPathOptions.DirectoryPath)));
return false;
}

return true;
}

private static bool ValidateFilePath(string filePath, string[] validExtensions, string errorMessage)
{
if (!validExtensions.Contains(Path.GetExtension(filePath)))
{
VSTestTrace.SafeWriteTrace(() => string.Format(errorMessage, filePath));
return false;
}

if (!File.Exists(filePath))
Copy link
Member

Choose a reason for hiding this comment

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

@baronfel do you have some helper around filesystem for mocking and more or is it fine to use this check directly?

{
VSTestTrace.SafeWriteTrace(() => string.Format(LocalizableStrings.CmdNonExistentFileErrorDescription, Path.GetFullPath(filePath)));
return false;
}

return true;
}

private async Task<int> RunBuild(string directoryPath)
{
bool solutionOrProjectFileFound = SolutionAndProjectUtility.TryGetProjectOrSolutionFilePath(directoryPath, out string projectOrSolutionFilePath, out bool isSolution);

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

public async Task<int> RunWithMSBuild(string filePath)
private async Task<int> RunBuild(string filePath, bool isSolution)
{
(IEnumerable<Module> modules, bool restored) = await GetProjectsProperties(filePath, false);
(IEnumerable<Module> modules, bool restored) = await GetProjectsProperties(filePath, isSolution);

InitializeTestApplications(modules);

Expand Down Expand Up @@ -92,7 +168,12 @@ public bool EnqueueTestApplications()

if (isSolution)
{
var projects = await SolutionAndProjectUtility.ParseSolution(solutionOrProjectFilePath);
string fileDirectory = Path.GetDirectoryName(solutionOrProjectFilePath);
string rootDirectory = string.IsNullOrEmpty(fileDirectory)
? Directory.GetCurrentDirectory()
: fileDirectory;

var projects = await SolutionAndProjectUtility.ParseSolution(solutionOrProjectFilePath, rootDirectory);
ProcessProjectsInParallel(projects, allProjects, ref restored);
}
else
Expand Down Expand Up @@ -178,7 +259,7 @@ private static IEnumerable<Module> ExtractModulesFromProject(Project project)
}
else
{
var frameworks = targetFrameworks.Split(Separator, StringSplitOptions.RemoveEmptyEntries);
var frameworks = targetFrameworks.Split(CliConstants.SemiColon, StringSplitOptions.RemoveEmptyEntries);
foreach (var framework in frameworks)
{
project.SetProperty(ProjectProperties.TargetFramework, framework);
Expand Down Expand Up @@ -225,8 +306,7 @@ private static BuildResult RestoreProject(string projectFilePath, ProjectCollect

private static bool IsBinaryLoggerEnabled(List<string> args, out string binLogFileName)
{
binLogFileName = BinLogFileName;

binLogFileName = string.Empty;
var binLogArgs = new List<string>();

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

if (binLogArg.Contains(':'))
if (binLogArg.Contains(CliConstants.Colon))
{
binLogFileName = binLogArg.Split(':')[1];
var parts = binLogArg.Split(CliConstants.Colon, 2);
binLogFileName = !string.IsNullOrEmpty(parts[1]) ? parts[1] : CliConstants.BinLogFileName;
}
else
{
binLogFileName = CliConstants.BinLogFileName;
}

return true;
}

Expand Down
9 changes: 9 additions & 0 deletions src/Cli/dotnet/commands/dotnet-test/Options.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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 BuildConfigurationOptions(bool HasNoRestore, bool HasNoBuild, string Configuration, string Architecture);

internal record BuildPathsOptions(string ProjectPath, string SolutionPath, string DirectoryPath);
}
77 changes: 30 additions & 47 deletions src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,80 +21,63 @@ public static bool TryGetProjectOrSolutionFilePath(string directory, out string
return false;
}

string[] possibleSolutionPaths = [
..Directory.GetFiles(directory, "*.sln", SearchOption.TopDirectoryOnly),
..Directory.GetFiles(directory, "*.slnx", SearchOption.TopDirectoryOnly)];
var possibleSolutionPaths = GetSolutionFilePaths(directory);

// If more than a single sln file is found, an error is thrown since we can't determine which one to choose.
if (possibleSolutionPaths.Length > 1)
{
VSTestTrace.SafeWriteTrace(() => string.Format(CommonLocalizableStrings.MoreThanOneSolutionInDirectory, directory));
return false;
}
// If a single solution is found, use it.
else if (possibleSolutionPaths.Length == 1)
{
// Get project file paths to check if there are any projects in the directory
string[] possibleProjectPaths = GetProjectFilePaths(directory);
// If more than a single sln file is found, an error is thrown since we can't determine which one to choose.
if (possibleSolutionPaths.Length > 1)
{
VSTestTrace.SafeWriteTrace(() => string.Format(CommonLocalizableStrings.MoreThanOneSolutionInDirectory, directory));
return false;
}

if (possibleSolutionPaths.Length == 1)
{
var possibleProjectPaths = GetProjectFilePaths(directory);

if (possibleProjectPaths.Length == 0)
{
projectOrSolutionFilePath = possibleSolutionPaths[0];
isSolution = true;
return true;
}
else // If both solution and project files are found, return false
{
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdMultipleProjectOrSolutionFilesErrorMessage);
return false;
}

VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdMultipleProjectOrSolutionFilesErrorDescription);
return false;
}
// If no solutions are found, look for a project file
else
else // If no solutions are found, look for a project file
{
string[] possibleProjectPath = GetProjectFilePaths(directory);

// No projects found throws an error that no sln nor projects were found
if (possibleProjectPath.Length == 0)
{
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdNoProjectOrSolutionFileErrorMessage);
VSTestTrace.SafeWriteTrace(() => LocalizableStrings.CmdNoProjectOrSolutionFileErrorDescription);
return false;
}
// A single project found, use it
else if (possibleProjectPath.Length == 1)

if (possibleProjectPath.Length == 1)
{
projectOrSolutionFilePath = possibleProjectPath[0];
return true;
}
// More than one project found. Not sure which one to choose
else
{
VSTestTrace.SafeWriteTrace(() => string.Format(CommonLocalizableStrings.MoreThanOneProjectInDirectory, directory));
return false;
}

VSTestTrace.SafeWriteTrace(() => string.Format(CommonLocalizableStrings.MoreThanOneProjectInDirectory, directory));

return false;
}
}


private static string[] GetProjectFilePaths(string directory)
private static string[] GetSolutionFilePaths(string directory)
{
var projectFiles = Directory.EnumerateFiles(directory, "*.*proj", SearchOption.TopDirectoryOnly)
.Where(IsProjectFile)
return Directory.EnumerateFiles(directory, CliConstants.SolutionExtensionPattern, SearchOption.TopDirectoryOnly)
.Concat(Directory.EnumerateFiles(directory, CliConstants.SolutionXExtensionPattern, SearchOption.TopDirectoryOnly))
.ToArray();

return projectFiles;
}

private static bool IsProjectFile(string filePath)
{
var extension = Path.GetExtension(filePath);
return extension.Equals(".csproj", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".vbproj", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".fsproj", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".proj", StringComparison.OrdinalIgnoreCase);
}
private static string[] GetProjectFilePaths(string directory) => [.. Directory.EnumerateFiles(directory, CliConstants.ProjectExtensionPattern, SearchOption.TopDirectoryOnly).Where(IsProjectFile)];

private static bool IsProjectFile(string filePath) => CliConstants.ProjectExtensions.Contains(Path.GetExtension(filePath), StringComparer.OrdinalIgnoreCase);

public static async Task<IEnumerable<string>> ParseSolution(string solutionFilePath)
public static async Task<IEnumerable<string>> ParseSolution(string solutionFilePath, string directory)
{
if (string.IsNullOrEmpty(solutionFilePath))
{
Expand All @@ -119,7 +102,7 @@ public static async Task<IEnumerable<string>> ParseSolution(string solutionFileP

if (solution is not null)
{
projectsPaths = [.. solution.SolutionProjects.Select(project => Path.GetFullPath(project.FilePath))];
projectsPaths.AddRange(solution.SolutionProjects.Select(project => Path.Combine(directory, project.FilePath)));
}

return projectsPaths;
Expand Down
Loading