From e3923a234f04eb81bf5689ad47b116b4e108e132 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 10 Jan 2025 13:53:05 -0600 Subject: [PATCH 1/7] Use new serializer library to parse solution files Because the library is async-only, this required a bit of bubbling-up of async signatures and CancellationToken-passing through the List and Why commands. --- Directory.Packages.props | 1 + .../ListPackage/ListPackageCommandRunner.cs | 6 ++++-- .../Commands/Why/WhyCommand.cs | 10 ++++++---- .../Commands/Why/WhyCommandArgs.cs | 7 ++++++- .../Commands/Why/WhyCommandRunner.cs | 15 +++++++++------ .../NuGet.CommandLine.XPlat.csproj | 1 + .../Utility/MSBuildAPIUtility.cs | 16 +++++++++++----- .../Utility/XPlatUtility.cs | 2 +- 8 files changed, 39 insertions(+), 19 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0376e87f3f2..72f2464d2a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -72,6 +72,7 @@ + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs index b7866d8dad2..1de2a43110d 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs @@ -58,8 +58,10 @@ public async Task ExecuteCommandAsync(ListPackageArgs listPackageArgs) //If the given file is a solution, get the list of projects //If not, then it's a project, which is put in a list - var projectsPaths = Path.GetExtension(listPackageArgs.Path).Equals(".sln", PathUtility.GetStringComparisonBasedOnOS()) ? - MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path).Where(f => File.Exists(f)) : + var projectsPaths = + (Path.GetExtension(listPackageArgs.Path).Equals(".sln", PathUtility.GetStringComparisonBasedOnOS()) || + Path.GetExtension(listPackageArgs.Path).Equals(".slnx", PathUtility.GetStringComparisonBasedOnOS())) ? + (await MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path, listPackageArgs.CancellationToken)).Where(f => File.Exists(f)) : new List(new string[] { listPackageArgs.Path }); MSBuildAPIUtility msBuild = listPackageReportModel.MSBuildAPIUtility; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs index 0619f2d05b6..799863f15e5 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs @@ -7,6 +7,7 @@ using System.CommandLine.Help; using System.CommandLine.Parsing; using System.IO; +using System.Threading.Tasks; using Microsoft.Extensions.CommandLineUtils; namespace NuGet.CommandLine.XPlat.Commands.Why @@ -36,7 +37,7 @@ public static void GetWhyCommand(CliCommand rootCommand) Register(rootCommand, CommandOutputLogger.Create, WhyCommandRunner.ExecuteCommand); } - internal static void Register(CliCommand rootCommand, Func getLogger, Func action) + internal static void Register(CliCommand rootCommand, Func getLogger, Func> action) { var whyCommand = new DocumentedCommand("why", Strings.WhyCommand_Description, "https://aka.ms/dotnet/nuget/why"); @@ -97,7 +98,7 @@ bool HasPathArgument(ArgumentResult ar) whyCommand.Options.Add(frameworks); whyCommand.Options.Add(help); - whyCommand.SetAction((parseResult) => + whyCommand.SetAction(async (parseResult, cancellationToken) => { ILoggerWithColor logger = getLogger(); @@ -107,9 +108,10 @@ bool HasPathArgument(ArgumentResult ar) parseResult.GetValue(path), parseResult.GetValue(package), parseResult.GetValue(frameworks), - logger); + logger, + cancellationToken); - int exitCode = action(whyCommandArgs); + int exitCode = await action(whyCommandArgs); return exitCode; } catch (ArgumentException ex) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs index 12029755d5a..e67f70b6d09 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Threading; namespace NuGet.CommandLine.XPlat.Commands.Why { @@ -14,6 +15,7 @@ internal class WhyCommandArgs public string Package { get; } public List Frameworks { get; } public ILoggerWithColor Logger { get; } + public CancellationToken CancellationToken { get; } /// /// A constructor for the arguments of the 'why' command. @@ -22,16 +24,19 @@ internal class WhyCommandArgs /// The package for which we show the dependency graphs. /// The target framework(s) for which we show the dependency graphs. /// + /// public WhyCommandArgs( string path, string package, List frameworks, - ILoggerWithColor logger) + ILoggerWithColor logger, + CancellationToken cancellationToken) { Path = path ?? throw new ArgumentNullException(nameof(path)); Package = package ?? throw new ArgumentNullException(nameof(package)); Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + CancellationToken = cancellationToken; // can't use null-coalescing because CancellationToken is a struct } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs index abf30ba33ae..8a4deb6a0e2 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs @@ -7,6 +7,9 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Build.Evaluation; using NuGet.ProjectModel; @@ -20,7 +23,7 @@ internal static class WhyCommandRunner /// Executes the 'why' command. /// /// CLI arguments for the 'why' command. - public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) + public static async Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { bool validArgumentsUsed = ValidatePathArgument(whyCommandArgs.Path, whyCommandArgs.Logger) && ValidatePackageArgument(whyCommandArgs.Package, whyCommandArgs.Logger); @@ -30,10 +33,10 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) } string targetPackage = whyCommandArgs.Package; - IEnumerable<(string assetsFilePath, string? projectPath)> assetsFiles; + IAsyncEnumerable<(string assetsFilePath, string? projectPath)> assetsFiles; try { - assetsFiles = FindAssetsFiles(whyCommandArgs.Path, whyCommandArgs.Logger); + assetsFiles = FindAssetsFilesAsync(whyCommandArgs.Path, whyCommandArgs.Logger, whyCommandArgs.CancellationToken); } catch (ArgumentException ex) { @@ -47,7 +50,7 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) } bool anyErrors = false; - foreach ((string assetsFilePath, string? projectPath) in assetsFiles) + await foreach ((string assetsFilePath, string? projectPath) in assetsFiles) { LockFile? assetsFile = GetProjectAssetsFile(assetsFilePath, projectPath, whyCommandArgs.Logger); @@ -88,7 +91,7 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) return anyErrors ? ExitCodes.Error : ExitCodes.Success; } - private static IEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFiles(string path, ILoggerWithColor logger) + private static async IAsyncEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFilesAsync(string path, ILoggerWithColor logger, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (XPlatUtility.IsJsonFile(path)) { @@ -96,7 +99,7 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) yield break; } - var projectPaths = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(path); + var projectPaths = await MSBuildAPIUtility.GetListOfProjectsFromPathArgumentAsync(path, cancellationToken); foreach (string projectPath in projectPaths.NoAllocEnumerate()) { Project project = MSBuildAPIUtility.GetProject(projectPath); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj index c9f2eddc5cd..1d182f7abf7 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj @@ -18,6 +18,7 @@ + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index 9cfc2da5aa8..7c23fc7b250 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -7,9 +7,14 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; +using Microsoft.VisualStudio.SolutionPersistence; +using Microsoft.VisualStudio.SolutionPersistence.Model; +using Microsoft.VisualStudio.SolutionPersistence.Serializer; using NuGet.Common; using NuGet.Frameworks; using NuGet.LibraryModel; @@ -79,10 +84,11 @@ private static Project GetProject(string projectCSProjPath, IDictionary GetProjectsFromSolution(string solutionPath) + internal static async Task> GetProjectsFromSolution(string solutionPath, CancellationToken cancellationToken = default) { - var sln = SolutionFile.Parse(solutionPath); - return sln.ProjectsInOrder.Select(p => p.AbsolutePath); + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solution = await serializer.OpenAsync(solutionPath, cancellationToken); + return solution.SolutionProjects.Select(p => p.FilePath); } /// @@ -90,7 +96,7 @@ internal static IEnumerable GetProjectsFromSolution(string solutionPath) /// /// List of project paths. Returns null if path was a directory with none or multiple project/solution files. /// Throws an exception if the directory has none or multiple project/solution files. - internal static IEnumerable GetListOfProjectsFromPathArgument(string path) + internal static async Task> GetListOfProjectsFromPathArgumentAsync(string path, CancellationToken cancellationToken = default) { string fullPath = Path.GetFullPath(path); @@ -116,7 +122,7 @@ internal static IEnumerable GetListOfProjectsFromPathArgument(string pat } return XPlatUtility.IsSolutionFile(projectOrSolutionFile) - ? MSBuildAPIUtility.GetProjectsFromSolution(projectOrSolutionFile).Where(f => File.Exists(f)) + ? (await MSBuildAPIUtility.GetProjectsFromSolution(projectOrSolutionFile, cancellationToken)).Where(f => File.Exists(f)) : [projectOrSolutionFile]; } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/XPlatUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/XPlatUtility.cs index 3727fba8fc4..72d2106a849 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/XPlatUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/XPlatUtility.cs @@ -132,7 +132,7 @@ internal static bool IsSolutionFile(string fileName) { var extension = System.IO.Path.GetExtension(fileName); - return string.Equals(extension, ".sln", StringComparison.OrdinalIgnoreCase); + return string.Equals(extension, ".sln", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".slnx", StringComparison.OrdinalIgnoreCase); } return false; From cff82e73b5f0c8fdfb8de984cf76f9c0803286e3 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 10 Jan 2025 16:34:15 -0600 Subject: [PATCH 2/7] Update Why and MSBuildAPIUtility tests due to signature change --- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 44 +++++++++++-------- .../Why/WhyCommandLineParsingTests.cs | 13 +++--- .../MSBuildAPIUtilityTests.cs | 41 ++++++++--------- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index 0ed7307d14f..ecc9b6d6a46 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading; using System.Threading.Tasks; using NuGet.CommandLine.XPlat; using NuGet.CommandLine.XPlat.Commands.Why; @@ -53,10 +54,11 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( project.ProjectPath, packageY.Id, [projectFramework], - logger); + logger, + CancellationToken.None); // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var output = logger.ShowMessages(); @@ -95,10 +97,11 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( project.ProjectPath, packageZ.Id, [projectFramework], - logger); + logger, + CancellationToken.None); // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var output = logger.ShowMessages(); @@ -108,7 +111,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } [Fact] - public void WhyCommand_ProjectDidNotRunRestore_Fails() + public async Task WhyCommand_ProjectDidNotRunRestore_Fails() { // Arrange var logger = new TestCommandOutputLogger(_testOutputHelper); @@ -128,10 +131,11 @@ public void WhyCommand_ProjectDidNotRunRestore_Fails() project.ProjectPath, packageY.Id, [projectFramework], - logger); + logger, + CancellationToken.None); // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var output = logger.ShowMessages(); @@ -141,7 +145,7 @@ public void WhyCommand_ProjectDidNotRunRestore_Fails() } [Fact] - public void WhyCommand_EmptyProjectArgument_Fails() + public async Task WhyCommand_EmptyProjectArgument_Fails() { // Arrange var logger = new TestCommandOutputLogger(_testOutputHelper); @@ -150,10 +154,11 @@ public void WhyCommand_EmptyProjectArgument_Fails() "", "PackageX", [], - logger); + logger, + CancellationToken.None); // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var errorOutput = logger.ShowErrors(); @@ -163,7 +168,7 @@ public void WhyCommand_EmptyProjectArgument_Fails() } [Fact] - public void WhyCommand_EmptyPackageArgument_Fails() + public async Task WhyCommand_EmptyPackageArgument_Fails() { // Arrange var logger = new TestCommandOutputLogger(_testOutputHelper); @@ -176,10 +181,11 @@ public void WhyCommand_EmptyPackageArgument_Fails() project.ProjectPath, "", [], - logger); + logger, + CancellationToken.None); // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var errorOutput = logger.ShowErrors(); @@ -189,7 +195,7 @@ public void WhyCommand_EmptyPackageArgument_Fails() } [Fact] - public void WhyCommand_InvalidProject_Fails() + public async Task WhyCommand_InvalidProject_Fails() { // Arrange var logger = new TestCommandOutputLogger(_testOutputHelper); @@ -200,10 +206,11 @@ public void WhyCommand_InvalidProject_Fails() fakeProjectPath, "PackageX", [], - logger); + logger, + CancellationToken.None); // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var errorOutput = logger.ShowErrors(); @@ -243,10 +250,11 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( project.ProjectPath, packageY.Id, [inputFrameworksOption, projectFramework], - logger); + logger, + CancellationToken.None); // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var output = logger.ShowMessages(); diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Why/WhyCommandLineParsingTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Why/WhyCommandLineParsingTests.cs index 29b83dac0c4..bb3742596cc 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Why/WhyCommandLineParsingTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Why/WhyCommandLineParsingTests.cs @@ -3,6 +3,7 @@ using System; using System.CommandLine; +using System.Threading.Tasks; using FluentAssertions; using NuGet.CommandLine.XPlat.Commands; using NuGet.CommandLine.XPlat.Commands.Why; @@ -38,7 +39,7 @@ public void WithTwoArguments_PathAndPackageAreSet() whyCommandArgs.Path.Should().Be(@"path\to\my.proj"); whyCommandArgs.Package.Should().Be("packageid"); whyCommandArgs.Frameworks.Should().BeNullOrEmpty(); - return 0; + return Task.FromResult(0); }); // Act @@ -59,7 +60,7 @@ public void WithOneArguments_PackageIsSet() whyCommandArgs.Path.Should().NotBeNull(); whyCommandArgs.Package.Should().Be("packageid"); whyCommandArgs.Frameworks.Should().BeNullOrEmpty(); - return 0; + return Task.FromResult(0); }); // Act @@ -117,7 +118,7 @@ public void FrameworkOption_CanBeAtAnyPosition(string args) whyCommandArgs.Path.Should().Be("my.proj"); whyCommandArgs.Package.Should().Be("packageid"); whyCommandArgs.Frameworks.Should().Equal(["net8.0"]); - return 0; + return Task.FromResult(0); }); // Act @@ -140,7 +141,7 @@ public void FrameworkOption_CanBeLongOrShortForm(string arg) whyCommandArgs.Path.Should().Be("my.proj"); whyCommandArgs.Package.Should().Be("packageid"); whyCommandArgs.Frameworks.Should().Equal(["net8.0"]); - return 0; + return Task.FromResult(0); }); // Act @@ -161,7 +162,7 @@ public void FrameworkOption_AcceptsMultipleValues() whyCommandArgs.Path.Should().Be("my.proj"); whyCommandArgs.Package.Should().Be("packageid"); whyCommandArgs.Frameworks.Should().Equal(["net8.0", "net481"]); - return 0; + return Task.FromResult(0); }); // Act @@ -182,7 +183,7 @@ public void HelpOption_ShowsHelp() whyCommandArgs.Path.Should().Be("my.proj"); whyCommandArgs.Package.Should().Be("packageid"); whyCommandArgs.Frameworks.Should().Equal(["net8.0", "net481"]); - return 0; + return Task.FromResult(0); }); // Act diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs index 377087e7dbc..c8eb6b0a031 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; using Microsoft.Build.Locator; @@ -57,8 +58,8 @@ public void GetDirectoryBuildPropsRootElementWhenItExists_Success() File.WriteAllText(Path.Combine(testDirectory, "Directory.Packages.props"), propsFile); string projectContent = -@$" - +@$" + net6.0 "; @@ -97,7 +98,7 @@ public void AddPackageReferenceIntoProjectFileWhenItemGroupDoesNotExist_Success( // Arrange project file string projectContent = @$" - + net6.0 "; @@ -151,7 +152,7 @@ public void AddPackageReferenceIntoProjectFileWhenItemGroupDoesExist_Success() // Arrange project file string projectContent = @$" - + net6.0 @@ -219,8 +220,8 @@ public void AddPackageVersionIntoPropsFileWhenItemGroupDoesNotExist_Success() // Arrange project file string projectContent = -@$" - +@$" + net6.0 "; @@ -287,8 +288,8 @@ public void AddPackageVersionIntoPropsFileWhenItemGroupExists_Success() // Arrange project file string projectContent = -@$" - +@$" + net6.0 @@ -356,8 +357,8 @@ public void UpdatePackageVersionInPropsFileWhenItExists_Success() // Arrange project file string projectContent = -@$" - +@$" + net6.0 @@ -459,7 +460,7 @@ public void UpdateVersionOverrideInPropsFileWhenItExists_Success() } [Fact] - public void GetListOfProjectsFromPathArgument_WithProjectFile_ReturnsCorrectPaths() + public async Task GetListOfProjectsFromPathArgument_WithProjectFile_ReturnsCorrectPaths() { // Arrange var pathContext = new SimpleTestPathContext(); @@ -470,7 +471,7 @@ public void GetListOfProjectsFromPathArgument_WithProjectFile_ReturnsCorrectPath projectA.Save(); // Act - var projectList = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(projectA.ProjectPath); + var projectList = await MSBuildAPIUtility.GetListOfProjectsFromPathArgumentAsync(projectA.ProjectPath); // Assert Assert.Equal(projectList.Count(), 1); @@ -478,7 +479,7 @@ public void GetListOfProjectsFromPathArgument_WithProjectFile_ReturnsCorrectPath } [Fact] - public void GetListOfProjectsFromPathArgument_WithProjectDirectory_ReturnsCorrectPaths() + public async Task GetListOfProjectsFromPathArgument_WithProjectDirectory_ReturnsCorrectPaths() { // Arrange var pathContext = new SimpleTestPathContext(); @@ -489,7 +490,7 @@ public void GetListOfProjectsFromPathArgument_WithProjectDirectory_ReturnsCorrec projectA.Save(); // Act - var projectList = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(Path.GetDirectoryName(projectA.ProjectPath)); + var projectList = await MSBuildAPIUtility.GetListOfProjectsFromPathArgumentAsync(Path.GetDirectoryName(projectA.ProjectPath)); // Assert Assert.Equal(projectList.Count(), 1); @@ -497,7 +498,7 @@ public void GetListOfProjectsFromPathArgument_WithProjectDirectory_ReturnsCorrec } [Fact] - public void GetListOfProjectsFromPathArgument_WithSolutionFile_ReturnsCorrectPaths() + public async Task GetListOfProjectsFromPathArgument_WithSolutionFile_ReturnsCorrectPaths() { // Arrange var pathContext = new SimpleTestPathContext(); @@ -512,7 +513,7 @@ public void GetListOfProjectsFromPathArgument_WithSolutionFile_ReturnsCorrectPat solution.Create(pathContext.SolutionRoot); // Act - var projectList = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(Path.GetDirectoryName(solution.SolutionPath)); + var projectList = await MSBuildAPIUtility.GetListOfProjectsFromPathArgumentAsync(Path.GetDirectoryName(solution.SolutionPath)); // Assert Assert.Equal(projectList.Count(), 2); @@ -521,7 +522,7 @@ public void GetListOfProjectsFromPathArgument_WithSolutionFile_ReturnsCorrectPat } [Fact] - public void GetListOfProjectsFromPathArgument_WithSolutionDirectory_ReturnsCorrectPaths() + public async Task GetListOfProjectsFromPathArgument_WithSolutionDirectory_ReturnsCorrectPaths() { // Arrange var pathContext = new SimpleTestPathContext(); @@ -536,7 +537,7 @@ public void GetListOfProjectsFromPathArgument_WithSolutionDirectory_ReturnsCorre solution.Create(pathContext.SolutionRoot); // Act - var projectList = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(pathContext.SolutionRoot); + var projectList = await MSBuildAPIUtility.GetListOfProjectsFromPathArgumentAsync(pathContext.SolutionRoot); // Assert Assert.Equal(projectList.Count(), 2); @@ -550,7 +551,7 @@ public void GetListOfProjectsFromPathArgument_WithSolutionDirectory_ReturnsCorre [InlineData("X.sln", "A.csproj")] [InlineData()] [InlineData("random.txt")] - public void GetListOfProjectsFromPathArgument_WithDirectoryWithInvalidNumberOfSolutionsOrProjects_ThrowsException(params string[] directoryFiles) + public async Task GetListOfProjectsFromPathArgument_WithDirectoryWithInvalidNumberOfSolutionsOrProjects_ThrowsException(params string[] directoryFiles) { // Arrange var pathContext = new SimpleTestPathContext(); @@ -563,7 +564,7 @@ public void GetListOfProjectsFromPathArgument_WithDirectoryWithInvalidNumberOfSo } // Act & Assert - Assert.Throws(() => MSBuildAPIUtility.GetListOfProjectsFromPathArgument(pathContext.SolutionRoot)); + await Assert.ThrowsAsync(() => MSBuildAPIUtility.GetListOfProjectsFromPathArgumentAsync(pathContext.SolutionRoot)); } [Fact] From d2dc33502c6153219caf3c91eb9345567842cf34 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 10 Jan 2025 16:34:32 -0600 Subject: [PATCH 3/7] Add SLNX generation and write a test showing list package support for both solution formats --- .../NuGet.XPlat.FuncTest/ListPackageTests.cs | 73 ++++++++++++-- .../SimpleTestSolutionContext.cs | 97 +++++++++++++------ 2 files changed, 130 insertions(+), 40 deletions(-) diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs index 1d2c0853ea3..9d3b4da9f52 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs @@ -261,17 +261,17 @@ static void SetupCredentialServiceMock(Mock mockedCredential return outCredentials != null; }); } + } - static async Task RestoreProjectsAsync(SimpleTestPathContext pathContext, SimpleTestProjectContext projectA, SimpleTestProjectContext projectB, ITestOutputHelper testOutputHelper) - { - var settings = Settings.LoadDefaultSettings(Path.GetDirectoryName(pathContext.SolutionRoot), Path.GetFileName(pathContext.NuGetConfig), null); - var packageSourceProvider = new PackageSourceProvider(settings); + static async Task RestoreProjectsAsync(SimpleTestPathContext pathContext, SimpleTestProjectContext projectA, SimpleTestProjectContext projectB, ITestOutputHelper testOutputHelper) + { + var settings = Settings.LoadDefaultSettings(Path.GetDirectoryName(pathContext.SolutionRoot), Path.GetFileName(pathContext.NuGetConfig), null); + var packageSourceProvider = new PackageSourceProvider(settings); - var sources = packageSourceProvider.LoadPackageSources(); + var sources = packageSourceProvider.LoadPackageSources(); - await RestoreProjectAsync(settings, pathContext, projectA, sources, testOutputHelper); - await RestoreProjectAsync(settings, pathContext, projectB, sources, testOutputHelper); - } + await RestoreProjectAsync(settings, pathContext, projectA, sources, testOutputHelper); + await RestoreProjectAsync(settings, pathContext, projectB, sources, testOutputHelper); static async Task RestoreProjectAsync(ISettings settings, SimpleTestPathContext pathContext, @@ -290,6 +290,63 @@ static async Task RestoreProjectAsync(ISettings settings, } } + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task CanListPackagesForProjectsInSolutions(bool useSlnx) + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + var packageA100 = new SimpleTestPackageContext("A", "1.0.0"); + var packageB100 = new SimpleTestPackageContext("B", "1.0.0"); + + await SimpleTestPackageUtility.CreatePackagesAsync( + pathContext.PackageSource, + packageA100, + packageB100); + + var projectA = SimpleTestProjectContext.CreateNETCore("ProjectA", pathContext.SolutionRoot, "net6.0"); + var projectB = SimpleTestProjectContext.CreateNETCore("ProjectB", pathContext.SolutionRoot, "net6.0"); + + projectA.AddPackageToAllFrameworks(packageA100); + projectB.AddPackageToAllFrameworks(packageB100); + + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot, useSlnx); + solution.Projects.Add(projectA); + solution.Projects.Add(projectB); + solution.Create(pathContext.SolutionRoot); + + using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, isPrivateFeed: true); + mockServer.Start(); + pathContext.Settings.AddSource(sourceName: "private-source", sourceUri: mockServer.ServiceIndexUri, allowInsecureConnectionsValue: bool.TrueString); + + // List package command requires restore to be run before it can list packages. + await RestoreProjectsAsync(pathContext, projectA, projectB, _testOutputHelper); + + var output = new StringBuilder(); + var error = new StringBuilder(); + using TextWriter consoleOut = new StringWriter(output); + using TextWriter consoleError = new StringWriter(error); + var logger = new TestLogger(_testOutputHelper); + ListPackageCommandRunner listPackageCommandRunner = new(); + var packageRefArgs = new ListPackageArgs( + path: solution.SolutionPath, + packageSources: [new(mockServer.ServiceIndexUri)], + frameworks: ["net6.0"], + reportType: ReportType.Vulnerable, + renderer: new ListPackageConsoleRenderer(consoleOut, consoleError), + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + logger: logger, + cancellationToken: CancellationToken.None); + + int result = await listPackageCommandRunner.ExecuteCommandAsync(packageRefArgs); + Assert.True(result == 0, userMessage: logger.ShowMessages()); + } + private void VerifyCommand(Action, CommandLineApplication, Func> verify) { // Arrange diff --git a/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestSolutionContext.cs b/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestSolutionContext.cs index e80b192195e..724e99b457f 100644 --- a/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestSolutionContext.cs +++ b/test/TestUtilities/Test.Utility/SimpleTestSetup/SimpleTestSolutionContext.cs @@ -14,9 +14,13 @@ namespace NuGet.Test.Utility /// public class SimpleTestSolutionContext { - public SimpleTestSolutionContext(string solutionRoot, params SimpleTestProjectContext[] projects) + public SimpleTestSolutionContext(string solutionRoot, params SimpleTestProjectContext[] projects) : this(solutionRoot, false, projects) { - SolutionPath = Path.Combine(solutionRoot, "solution.sln"); + } + + public SimpleTestSolutionContext(string solutionRoot, bool useSlnx, params SimpleTestProjectContext[] projects) + { + SolutionPath = Path.Combine(solutionRoot, useSlnx ? "solution.slnx" : "solution.sln"); Projects.AddRange(projects); } @@ -50,42 +54,71 @@ public void Save(string path) public StringBuilder GetContent() { - StringBuilder sb = new StringBuilder(); - - sb.AppendLine("Microsoft Visual Studio Solution File, Format Version 12.00"); - sb.AppendLine("# Visual Studio 2012"); + if (Path.GetExtension(SolutionPath) == ".sln") + { + return GetContentForSln(); + } + else if (Path.GetExtension(SolutionPath) == ".slnx") + { + return GetContentForSlnx(); + } + else + { + throw new InvalidOperationException("Unknown solution file type"); + } - foreach (var project in Projects) + StringBuilder GetContentForSln() { - sb.AppendLine("Project(\"{" + "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC" + "}" - + $"\") = \"{project.ProjectName}\", " + "\"" + project.ProjectPath + "\", \"{" + project.ProjectGuid.ToString().ToUpperInvariant() + "}\""); - sb.AppendLine("EndProject"); + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("Microsoft Visual Studio Solution File, Format Version 12.00"); + sb.AppendLine("# Visual Studio 2012"); + + foreach (var project in Projects) + { + sb.AppendLine("Project(\"{" + "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC" + "}" + + $"\") = \"{project.ProjectName}\", " + "\"" + project.ProjectPath + "\", \"{" + project.ProjectGuid.ToString().ToUpperInvariant() + "}\""); + sb.AppendLine("EndProject"); + } + + sb.AppendLine("Global"); + sb.AppendLine(" GlobalSection(SolutionConfigurationPlatforms) = preSolution"); + sb.AppendLine(" Debug|Any CPU = Debug|Any CPU"); + sb.AppendLine(" Release|Any CPU = Release|Any CPU"); + sb.AppendLine(" EndGlobalSection"); + sb.AppendLine(" GlobalSection(ProjectConfigurationPlatforms) = postSolution"); + foreach (var project in Projects) + { + // this should probably be uppercase? + sb.AppendLine(" {" + project.ProjectGuid.ToString().ToUpperInvariant() + "}.Debug|Any CPU.ActiveCfg = Debug|Any CPU"); + sb.AppendLine(" {" + project.ProjectGuid.ToString().ToUpperInvariant() + "}.Debug|Any CPU.Build.0 = Debug|Any CPU"); + sb.AppendLine(" {" + project.ProjectGuid.ToString().ToUpperInvariant() + "}.Release|Any CPU.ActiveCfg = Release|Any CPU"); + sb.AppendLine(" {" + project.ProjectGuid.ToString().ToUpperInvariant() + "}.Release|Any CPU.Build.0 = Release|Any CPU"); + } + sb.AppendLine(" EndGlobalSection"); + sb.AppendLine(" GlobalSection(SolutionProperties) = preSolution"); + sb.AppendLine(" HideSolutionNode = FALSE"); + sb.AppendLine(" EndGlobalSection"); + sb.AppendLine(" GlobalSection(ExtensibilityGlobals) = postSolution"); + sb.AppendLine(" SolutionGuid = {" + SolutionGuid.ToString() + "}"); + sb.AppendLine(" EndGlobalSection"); + sb.AppendLine("EndGlobal"); + + return sb; } - sb.AppendLine("Global"); - sb.AppendLine(" GlobalSection(SolutionConfigurationPlatforms) = preSolution"); - sb.AppendLine(" Debug|Any CPU = Debug|Any CPU"); - sb.AppendLine(" Release|Any CPU = Release|Any CPU"); - sb.AppendLine(" EndGlobalSection"); - sb.AppendLine(" GlobalSection(ProjectConfigurationPlatforms) = postSolution"); - foreach (var project in Projects) + StringBuilder GetContentForSlnx() { - // this should probably be uppercase? - sb.AppendLine(" {" + project.ProjectGuid.ToString().ToUpperInvariant() + "}.Debug|Any CPU.ActiveCfg = Debug|Any CPU"); - sb.AppendLine(" {" + project.ProjectGuid.ToString().ToUpperInvariant() + "}.Debug|Any CPU.Build.0 = Debug|Any CPU"); - sb.AppendLine(" {" + project.ProjectGuid.ToString().ToUpperInvariant() + "}.Release|Any CPU.ActiveCfg = Release|Any CPU"); - sb.AppendLine(" {" + project.ProjectGuid.ToString().ToUpperInvariant() + "}.Release|Any CPU.Build.0 = Release|Any CPU"); + StringBuilder sb = new StringBuilder(); + sb.AppendLine(""); + foreach (var project in Projects) + { + sb.AppendLine($""); + } + sb.AppendLine(""); + + return sb; } - sb.AppendLine(" EndGlobalSection"); - sb.AppendLine(" GlobalSection(SolutionProperties) = preSolution"); - sb.AppendLine(" HideSolutionNode = FALSE"); - sb.AppendLine(" EndGlobalSection"); - sb.AppendLine(" GlobalSection(ExtensibilityGlobals) = postSolution"); - sb.AppendLine(" SolutionGuid = {" + SolutionGuid.ToString() + "}"); - sb.AppendLine(" EndGlobalSection"); - sb.AppendLine("EndGlobal"); - - return sb; } /// From 34ad2938fd205b5723c7b81ca0825e547682b214 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 10 Jan 2025 16:54:05 -0600 Subject: [PATCH 4/7] make the slnx parser come from dotnet-public --- NuGet.Config | 1 + 1 file changed, 1 insertion(+) diff --git a/NuGet.Config b/NuGet.Config index 112c015a6b3..2fdd17888c8 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -30,6 +30,7 @@ + From 58914048577f0e150895b7b4665d983caed64735 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 10 Jan 2025 23:51:17 -0600 Subject: [PATCH 5/7] Remove extraneous comment --- .../NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs index e67f70b6d09..d8c6ddc6022 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs @@ -36,7 +36,7 @@ public WhyCommandArgs( Package = package ?? throw new ArgumentNullException(nameof(package)); Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - CancellationToken = cancellationToken; // can't use null-coalescing because CancellationToken is a struct + CancellationToken = cancellationToken; } } } From b535b1028eeda7990f5ec8d9f08b15e605078acc Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 10 Jan 2025 23:53:37 -0600 Subject: [PATCH 6/7] sourcebuild exclusion glob --- eng/SourceBuildPrebuiltBaseline.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml index 292ac5e11ee..ace032bcf0b 100644 --- a/eng/SourceBuildPrebuiltBaseline.xml +++ b/eng/SourceBuildPrebuiltBaseline.xml @@ -46,6 +46,7 @@ + From d182ff9738098b5f7c7d0639867c941a94b30fe8 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sat, 11 Jan 2025 09:29:59 -0600 Subject: [PATCH 7/7] mark SLNX library as source-build-allowable, since it is part of source-build-externals --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index 72f2464d2a4..478afbd2f5d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -193,6 +193,7 @@ <_allowBuildFromSourcePackage Include="Microsoft.Extensions.CommandLineUtils.Sources" /> <_allowBuildFromSourcePackage Include="Microsoft.Extensions.FileProviders.Abstractions" /> <_allowBuildFromSourcePackage Include="Microsoft.Extensions.FileSystemGlobbing" /> + <_allowBuildFromSourcePackage Include="Microsoft.VisualStudio.SolutionPersistence" /> <_allowBuildFromSourcePackage Include="Microsoft.Web.Xdt" /> <_allowBuildFromSourcePackage Include="Newtonsoft.Json" /> <_allowBuildFromSourcePackage Include="System.Collections.Immutable" />