diff --git a/Directory.Packages.props b/Directory.Packages.props index 5a4aad32136..27058b57833 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -84,6 +84,8 @@ + + @@ -179,6 +181,7 @@ <_allowBuildFromSourcePackage Include="Microsoft.Extensions.FileSystemGlobbing" /> <_allowBuildFromSourcePackage Include="Microsoft.Web.Xdt" /> <_allowBuildFromSourcePackage Include="Newtonsoft.Json" /> + <_allowBuildFromSourcePackage Include="Spectre.Console" /> <_allowBuildFromSourcePackage Include="System.Collections.Immutable" /> <_allowBuildFromSourcePackage Include="System.CommandLine" /> <_allowBuildFromSourcePackage Include="System.ComponentModel.Composition" /> diff --git a/NuGet.Config b/NuGet.Config index 68f75ca225c..07a8d89d473 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -41,6 +41,7 @@ + diff --git a/NuGet.sln b/NuGet.sln index 6e3dd987608..89ddd5d7f97 100644 --- a/NuGet.sln +++ b/NuGet.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution build\config.props = build\config.props Directory.Packages.props = Directory.Packages.props build\DotNetSdkVersions.txt = build\DotNetSdkVersions.txt + NuGet.Config = NuGet.Config build\sign.targets = build\sign.targets spelling.dic = spelling.dic build\test.targets = build\test.targets diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/DependencyGraphPrinter.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/DependencyGraphPrinter.cs index 7cf95afd387..61e0f1b1e4e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/DependencyGraphPrinter.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/DependencyGraphPrinter.cs @@ -7,19 +7,14 @@ using System.Collections.Generic; using System.Linq; using NuGet.Shared; +using Spectre.Console; +using Spectre.Console.Rendering; namespace NuGet.CommandLine.XPlat.Commands.Why { internal static class DependencyGraphPrinter { - private const ConsoleColor TargetPackageColor = ConsoleColor.Cyan; - - // Dependency graph console output symbols - private const string ChildNodeSymbol = "├─ "; - private const string LastChildNodeSymbol = "└─ "; - - private const string ChildPrefixSymbol = "│ "; - private const string LastChildPrefixSymbol = " "; + private static readonly Color TargetPackageColor = Color.Cyan; /// /// Prints the dependency graphs for all target frameworks. @@ -27,10 +22,10 @@ internal static class DependencyGraphPrinter /// A dictionary mapping target frameworks to their dependency graphs. /// The package we want the dependency paths for. /// - public static void PrintAllDependencyGraphs(Dictionary?> dependencyGraphPerFramework, string targetPackage, ILoggerWithColor logger) + public static void PrintAllDependencyGraphs(Dictionary?> dependencyGraphPerFramework, string targetPackage, IAnsiConsole logger) { // print empty line - logger.LogMinimal(""); + logger.WriteLine(); // deduplicate the dependency graphs List> deduplicatedFrameworks = GetDeduplicatedFrameworks(dependencyGraphPerFramework); @@ -51,29 +46,27 @@ public static void PrintAllDependencyGraphs(DictionaryThe top-level package nodes of the dependency graph. /// The package we want the dependency paths for. /// - private static void PrintDependencyGraphPerFramework(List frameworks, List? topLevelNodes, string targetPackage, ILoggerWithColor logger) + private static void PrintDependencyGraphPerFramework(List frameworks, List? topLevelNodes, string targetPackage, IAnsiConsole logger) { - // print framework header - foreach (var framework in frameworks) - { - logger.LogMinimal($" [{framework}]"); - } - - logger.LogMinimal($" {ChildPrefixSymbol}"); + var tree = new Tree(string.Join("\n", frameworks.Select(f => $"[[{f}]]"))); if (topLevelNodes == null || topLevelNodes.Count == 0) { - logger.LogMinimal($" {LastChildNodeSymbol}{Strings.WhyCommand_Message_NoDependencyGraphsFoundForFramework}\n\n"); + tree.AddNode(Strings.WhyCommand_Message_NoDependencyGraphsFoundForFramework); + logger.Write(PadTree(tree)); return; } var stack = new Stack(); // initialize the stack with all top-level nodes - int counter = 0; foreach (var node in topLevelNodes.OrderByDescending(c => c.Id, StringComparer.OrdinalIgnoreCase)) { - stack.Push(new StackOutputData(node, prefix: " ", isLastChild: counter++ == 0)); + stack.Push(new StackOutputData + { + Node = node, + ParentNode = tree + }); } // print the dependency graph @@ -81,41 +74,39 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li { var current = stack.Pop(); - string currentPrefix, childPrefix; - if (current.IsLastChild) - { - currentPrefix = current.Prefix + LastChildNodeSymbol; - childPrefix = current.Prefix + LastChildPrefixSymbol; - } - else - { - currentPrefix = current.Prefix + ChildNodeSymbol; - childPrefix = current.Prefix + ChildPrefixSymbol; - } - - // print current node - if (current.Node.Id.Equals(targetPackage, StringComparison.OrdinalIgnoreCase)) - { - logger.LogMinimal($"{currentPrefix}", Console.ForegroundColor); - logger.LogMinimal($"{current.Node.Id} (v{current.Node.Version})\n", TargetPackageColor); - } - else - { - logger.LogMinimal($"{currentPrefix}{current.Node.Id} (v{current.Node.Version})"); - } + var treeNodeText = GetNodeText(current.Node, targetPackage); + var treeNode = current.ParentNode.AddNode(treeNodeText); if (current.Node.Children?.Count > 0) { - // push all the node's children onto the stack - counter = 0; foreach (var child in current.Node.Children.OrderByDescending(c => c.Id, StringComparer.OrdinalIgnoreCase)) { - stack.Push(new StackOutputData(child, childPrefix, isLastChild: counter++ == 0)); + stack.Push(new StackOutputData + { + Node = child, + ParentNode = treeNode + }); } } } - logger.LogMinimal(""); + logger.Write(PadTree(tree)); + logger.WriteLine(); + } + + private static IRenderable GetNodeText(DependencyNode node, string targetPackage) + { + string text = $"{node.Id} (v{node.Version})"; + Style? style = node.Id.Equals(targetPackage, StringComparison.OrdinalIgnoreCase) + ? new Style(foreground: TargetPackageColor) + : null; + + return new Text(text, style); + } + + private static IRenderable PadTree(Tree tree) + { + return new Padder(tree, new Padding(left: 2, 0, 0, 0)); } /// @@ -173,16 +164,9 @@ private static int GetDependencyGraphHashCode(List? graph) private class StackOutputData { - public DependencyNode Node { get; set; } - public string Prefix { get; set; } - public bool IsLastChild { get; set; } + public required DependencyNode Node { get; init; } - public StackOutputData(DependencyNode node, string prefix, bool isLastChild) - { - Node = node; - Prefix = prefix; - IsLastChild = isLastChild; - } + public required IHasTreeNodes ParentNode { get; init; } } } } 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 3acb50281ef..fac895af5ed 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs @@ -9,6 +9,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.CommandLineUtils; +using Spectre.Console; namespace NuGet.CommandLine.XPlat.Commands.Why { @@ -22,9 +23,9 @@ internal static void Register(CommandLineApplication app) }); } - internal static void Register(Command rootCommand, Func getLogger) + internal static void Register(Command rootCommand, IAnsiConsole console) { - Register(rootCommand, getLogger, WhyCommandRunner.ExecuteCommand); + Register(rootCommand, console, WhyCommandRunner.ExecuteCommand); } /// @@ -34,10 +35,12 @@ internal static void Register(Command rootCommand, Func getLog /// The dotnet nuget command handler, to add why to. public static void GetWhyCommand(Command rootCommand) { - Register(rootCommand, CommandOutputLogger.Create, WhyCommandRunner.ExecuteCommand); + Register(rootCommand, + Spectre.Console.AnsiConsole.Console, + WhyCommandRunner.ExecuteCommand); } - internal static void Register(Command rootCommand, Func getLogger, Func> action) + internal static void Register(Command rootCommand, IAnsiConsole console, Func> action) { var whyCommand = new DocumentedCommand("why", Strings.WhyCommand_Description, "https://aka.ms/dotnet/nuget/why"); @@ -100,15 +103,13 @@ bool HasPathArgument(ArgumentResult ar) whyCommand.SetAction(async (parseResult, cancellationToken) => { - ILoggerWithColor logger = getLogger(); - try { var whyCommandArgs = new WhyCommandArgs( - parseResult.GetValue(path), - parseResult.GetValue(package), - parseResult.GetValue(frameworks), - logger, + parseResult.GetValue(path)!, + parseResult.GetValue(package)!, + parseResult.GetValue(frameworks)!, + console, cancellationToken); int exitCode = await action(whyCommandArgs); @@ -116,7 +117,7 @@ bool HasPathArgument(ArgumentResult ar) } catch (ArgumentException ex) { - logger.LogError(ex.Message); + console.Markup($"[red]{ex.Message}[/]"); return ExitCodes.InvalidArguments; } }); 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 d8c6ddc6022..db4a7efd335 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Threading; +using Spectre.Console; namespace NuGet.CommandLine.XPlat.Commands.Why { @@ -14,7 +15,7 @@ internal class WhyCommandArgs public string Path { get; } public string Package { get; } public List Frameworks { get; } - public ILoggerWithColor Logger { get; } + public IAnsiConsole Logger { get; } public CancellationToken CancellationToken { get; } /// @@ -29,7 +30,7 @@ public WhyCommandArgs( string path, string package, List frameworks, - ILoggerWithColor logger, + IAnsiConsole logger, CancellationToken cancellationToken) { Path = path ?? throw new ArgumentNullException(nameof(path)); 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 aa54ce78a47..950329cfffd 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.Build.Evaluation; using NuGet.ProjectModel; +using Spectre.Console; namespace NuGet.CommandLine.XPlat.Commands.Why { @@ -38,11 +39,11 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) } catch (ArgumentException ex) { - whyCommandArgs.Logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentExceptionThrown, - ex.Message)); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentExceptionThrown, + ex.Message); + whyCommandArgs.Logger.MarkupLine($"[red]{message}[/]"); return Task.FromResult(ExitCodes.InvalidArguments); } @@ -63,7 +64,7 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) if (dependencyGraphPerFramework != null) { - whyCommandArgs.Logger.LogMinimal( + whyCommandArgs.Logger.WriteLine( string.Format(CultureInfo.CurrentCulture, Strings.WhyCommand_Message_DependencyGraphsFoundInProject, assetsFile.PackageSpec.Name, @@ -73,7 +74,7 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) } else { - whyCommandArgs.Logger.LogMinimal( + whyCommandArgs.Logger.WriteLine( string.Format(CultureInfo.CurrentCulture, Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, assetsFile.PackageSpec.Name, @@ -89,7 +90,7 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) return Task.FromResult(anyErrors ? ExitCodes.Error : ExitCodes.Success); } - private static IEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFiles(string path, ILoggerWithColor logger) + private static IEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFiles(string path, IAnsiConsole logger) { if (XPlatUtility.IsJsonFile(path)) { @@ -108,23 +109,22 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.Error_NotPRProject, - project.FullPath)); - + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.Error_NotPRProject, + project.FullPath); + logger.MarkupLine($"[red]{message}[/]"); continue; } string? assetsFilePath = project.GetPropertyValue(ProjectAssetsFile); if (string.IsNullOrEmpty(assetsFilePath) || !File.Exists(assetsFilePath)) { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.Error_AssetsFileNotFound, - project.FullPath)); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.Error_AssetsFileNotFound, + project.FullPath); + logger.MarkupLine($"[red]{message}[/]"); continue; } @@ -132,7 +132,7 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) } else { - logger.LogMinimal( + logger.WriteLine( string.Format( CultureInfo.CurrentCulture, Strings.WhyCommand_Message_NonSDKStyleProjectsAreNotSupported, @@ -149,15 +149,15 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) /// /// Validates that the input 'path' argument is a valid path to a directory, solution file or project file. /// - private static bool ValidatePathArgument(string path, ILoggerWithColor logger) + private static bool ValidatePathArgument(string path, IAnsiConsole logger) { if (string.IsNullOrEmpty(path)) { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - "PROJECT|SOLUTION")); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + "PROJECT|SOLUTION"); + logger.MarkupLine($"[red]{message}[/]"); return false; } @@ -169,11 +169,11 @@ private static bool ValidatePathArgument(string path, ILoggerWithColor logger) } catch (ArgumentException) { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentExceptionThrown, - string.Format(CultureInfo.CurrentCulture, Strings.Error_PathIsMissingOrInvalid, path))); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentExceptionThrown, + string.Format(CultureInfo.CurrentCulture, Strings.Error_PathIsMissingOrInvalid, path)); + logger.MarkupLine($"[red]{message}[/]"); return false; } @@ -186,24 +186,24 @@ private static bool ValidatePathArgument(string path, ILoggerWithColor logger) } else { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentExceptionThrown, - string.Format(CultureInfo.CurrentCulture, Strings.Error_PathIsMissingOrInvalid, path))); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentExceptionThrown, + string.Format(CultureInfo.CurrentCulture, Strings.Error_PathIsMissingOrInvalid, path)); + logger.MarkupLine($"[red]{message}[/]"); return false; } } - private static bool ValidatePackageArgument(string package, ILoggerWithColor logger) + private static bool ValidatePackageArgument(string package, IAnsiConsole logger) { if (string.IsNullOrEmpty(package)) { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - "PACKAGE")); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + "PACKAGE"); + logger.MarkupLine($"[red]{message}[/]"); return false; } @@ -213,19 +213,19 @@ private static bool ValidatePackageArgument(string package, ILoggerWithColor log /// /// Validates that the input frameworks options have corresponding targets in the assets file. Outputs a warning message if a framework does not exist. /// - private static void ValidateFrameworksOptionsExistInAssetsFile(LockFile assetsFile, List inputFrameworks, ILoggerWithColor logger) + private static void ValidateFrameworksOptionsExistInAssetsFile(LockFile assetsFile, List inputFrameworks, IAnsiConsole logger) { foreach (var frameworkAlias in inputFrameworks) { if (assetsFile.GetTarget(frameworkAlias, runtimeIdentifier: null) == null) { - logger.LogWarning( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Warning_AssetsFileDoesNotContainSpecifiedTarget, - assetsFile.Path, - assetsFile.PackageSpec.Name, - frameworkAlias)); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Warning_AssetsFileDoesNotContainSpecifiedTarget, + assetsFile.Path, + assetsFile.PackageSpec.Name, + frameworkAlias); + logger.MarkupLine($"[yellow]{message}[/]"); } } } @@ -237,25 +237,25 @@ private static void ValidateFrameworksOptionsExistInAssetsFile(LockFile assetsFi /// /// Logger for the 'why' command /// Assets file for the given project. Returns null if there was any issue finding or parsing the assets file. - private static LockFile? GetProjectAssetsFile(string assetsFilePath, string? projectPath, ILoggerWithColor logger) + private static LockFile? GetProjectAssetsFile(string assetsFilePath, string? projectPath, IAnsiConsole logger) { if (!File.Exists(assetsFilePath)) { if (!string.IsNullOrEmpty(projectPath)) { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.Error_AssetsFileNotFound, - projectPath)); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.Error_AssetsFileNotFound, + projectPath); + logger.MarkupLine($"[red]{message}[/]"); } else { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.Error_PathIsMissingOrInvalid, - assetsFilePath)); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.Error_PathIsMissingOrInvalid, + assetsFilePath); + logger.MarkupLine($"[red]{message}[/]"); } return null; @@ -271,20 +271,20 @@ private static void ValidateFrameworksOptionsExistInAssetsFile(LockFile assetsFi { if (string.IsNullOrEmpty(projectPath)) { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_InvalidAssetsFile_WithoutProject, - assetsFilePath)); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_InvalidAssetsFile_WithoutProject, + assetsFilePath); + logger.MarkupLine($"[red]{message}[/]"); } else { - logger.LogError( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_InvalidAssetsFile_WithProject, - assetsFilePath, - projectPath)); + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_InvalidAssetsFile_WithProject, + assetsFilePath, + projectPath); + logger.MarkupLine($"[red]{message}[/]"); } return null; 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 850e30c7035..118c2aca8c6 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj @@ -17,6 +17,7 @@ + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index f625524ad53..d34900d0f4f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -108,8 +108,8 @@ public static int MainInternal(string[] args, CommandOutputLogger log, IEnvironm ConfigCommand.Register(nugetCommand, getHidePrefixLogger); ConfigCommand.Register(rootCommand, getHidePrefixLogger); - Commands.Why.WhyCommand.Register(nugetCommand, getHidePrefixLogger); - Commands.Why.WhyCommand.Register(rootCommand, getHidePrefixLogger); + Commands.Why.WhyCommand.Register(nugetCommand, Spectre.Console.AnsiConsole.Console); + Commands.Why.WhyCommand.Register(rootCommand, Spectre.Console.AnsiConsole.Console); } CancellationTokenSource tokenSource = new CancellationTokenSource(); diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index 2519be13095..2ed30711691 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -56,7 +56,7 @@ await SimpleTestPackageUtility.CreatePackagesAsync( // Assert Assert.Equal(ExitCodes.Success, result.ExitCode); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput.Replace("\n", "").Replace("\r", "")); } [Fact] @@ -118,7 +118,7 @@ await SimpleTestPackageUtility.CreatePackagesAsync( // Assert Assert.Equal(ExitCodes.Success, result.ExitCode); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput.Replace("\n", "").Replace("\r", "")); } [Fact] @@ -150,7 +150,7 @@ await SimpleTestPackageUtility.CreatePackagesAsync( // Assert Assert.Equal(ExitCodes.Success, result.ExitCode); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput.Replace("\n", "").Replace("\r", "")); } [Fact] @@ -186,40 +186,6 @@ public void WhyCommand_EmptyPackageArgument_Fails() Assert.Contains($"Required argument missing for command: 'why'.", result.Errors); } - [Fact] - public async Task WhyCommand_InvalidFrameworksOption_WarnsCorrectly() - { - // Arrange - var pathContext = new SimpleTestPathContext(); - var inputFrameworksOption = "invalidFrameworkAlias"; - var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, Constants.ProjectTargetFramework); - - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", Constants.ProjectTargetFramework); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1", Constants.ProjectTargetFramework); - - packageX.Dependencies.Add(packageY); - - project.AddPackageToFramework(Constants.ProjectTargetFramework, packageX); - - await SimpleTestPackageUtility.CreatePackagesAsync( - pathContext.PackageSource, - packageX, - packageY); - - string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; - CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs, testOutputHelper: _testOutputHelper); - - string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id} -f {inputFrameworksOption} -f {Constants.ProjectTargetFramework}"; - - // Act - CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs, testOutputHelper: _testOutputHelper); - - // Assert - Assert.Equal(ExitCodes.Success, result.ExitCode); - Assert.Contains($"warn : The assets file '{project.AssetsFileOutputPath}' for project '{ProjectName}' does not contain a target for the specified input framework '{inputFrameworksOption}'.", result.AllOutput); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); - } - [Fact] public async Task WhyCommand_DirectoryWithProject_HasTransitiveDependency_DependencyPathExists() { @@ -250,7 +216,7 @@ await SimpleTestPackageUtility.CreatePackagesAsync( // Assert Assert.Equal(ExitCodes.Success, result.ExitCode); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); + result.AllOutput.Replace("\n", "").Replace("\r", "").Should().Contain($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'"); } [Fact] diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/NuGet.XPlat.FuncTest.csproj b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/NuGet.XPlat.FuncTest.csproj index 73610909851..4d55c7a887b 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/NuGet.XPlat.FuncTest.csproj +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/NuGet.XPlat.FuncTest.csproj @@ -1,4 +1,4 @@ - + $(LatestNETCoreTargetFramework) Functional tests for nuget in dotnet CLI scenarios, using the NuGet.CommandLine.XPlat assembly. @@ -13,6 +13,10 @@ + + + + diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index ecc9b6d6a46..2003e977c2c 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -3,10 +3,12 @@ using System.Threading; using System.Threading.Tasks; +using FluentAssertions; using NuGet.CommandLine.XPlat; using NuGet.CommandLine.XPlat.Commands.Why; using NuGet.Packaging; using NuGet.Test.Utility; +using Spectre.Console.Testing; using Xunit; using Xunit.Abstractions; @@ -50,23 +52,35 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, new MSBuildAPIUtility(logger)); + var console = new TestConsole(); + console.Width(100); + var whyCommandArgs = new WhyCommandArgs( project.ProjectPath, packageY.Id, [projectFramework], - logger, + console, CancellationToken.None); // Act var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var output = logger.ShowMessages(); + var output = console.Output; + + string[] expected = + [ + "Project 'Test.Project.DotnetNugetWhy' has the following dependency graph(s) for 'PackageY':", + "", + " [net472] ", + " └── PackageX (v1.0.0) ", + " └── PackageY (v1.0.1) ", + "", + "" + ]; Assert.Equal(ExitCodes.Success, result); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); - Assert.Contains($"{packageX.Id} (v{packageX.Version})", output); - Assert.Contains($"{packageY.Id} (v{packageY.Version})", output); + output.Should().Be(string.Join("\n", expected)); } [Fact] @@ -93,18 +107,21 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, new MSBuildAPIUtility(logger)); + var console = new TestConsole(); + console.Width(500); + var whyCommandArgs = new WhyCommandArgs( project.ProjectPath, packageZ.Id, [projectFramework], - logger, + console, CancellationToken.None); // Act var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var output = logger.ShowMessages(); + var output = console.Output; Assert.Equal(ExitCodes.Success, result); Assert.Contains($"Project '{ProjectName}' does not have a dependency on '{packageZ.Id}'", output); @@ -114,7 +131,8 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( public async Task WhyCommand_ProjectDidNotRunRestore_Fails() { // Arrange - var logger = new TestCommandOutputLogger(_testOutputHelper); + var logger = new TestConsole(); + logger.Width(500); var pathContext = new SimpleTestPathContext(); var projectFramework = "net472"; @@ -138,7 +156,7 @@ public async Task WhyCommand_ProjectDidNotRunRestore_Fails() var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var output = logger.ShowMessages(); + var output = logger.Lines; Assert.Equal(ExitCodes.Success, result); Assert.Contains($"No assets file was found for `{project.ProjectPath}`. Please run restore before running this command.", output); @@ -148,7 +166,8 @@ public async Task WhyCommand_ProjectDidNotRunRestore_Fails() public async Task WhyCommand_EmptyProjectArgument_Fails() { // Arrange - var logger = new TestCommandOutputLogger(_testOutputHelper); + var logger = new TestConsole(); + logger.Width(500); var whyCommandArgs = new WhyCommandArgs( "", @@ -161,17 +180,17 @@ public async Task WhyCommand_EmptyProjectArgument_Fails() var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var errorOutput = logger.ShowErrors(); + var errorOutput = logger.Lines; Assert.Equal(ExitCodes.InvalidArguments, result); - Assert.Contains($"Unable to run 'dotnet nuget why'. The 'PROJECT|SOLUTION' argument cannot be empty.", errorOutput); + errorOutput.Should().Contain($"Unable to run 'dotnet nuget why'. The 'PROJECT|SOLUTION' argument cannot be empty."); } [Fact] public async Task WhyCommand_EmptyPackageArgument_Fails() { // Arrange - var logger = new TestCommandOutputLogger(_testOutputHelper); + var logger = new TestConsole(); var pathContext = new SimpleTestPathContext(); var projectFramework = "net472"; @@ -188,7 +207,7 @@ public async Task WhyCommand_EmptyPackageArgument_Fails() var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var errorOutput = logger.ShowErrors(); + var errorOutput = logger.Lines; Assert.Equal(ExitCodes.InvalidArguments, result); Assert.Contains($"Unable to run 'dotnet nuget why'. The 'PACKAGE' argument cannot be empty.", errorOutput); @@ -198,7 +217,8 @@ public async Task WhyCommand_EmptyPackageArgument_Fails() public async Task WhyCommand_InvalidProject_Fails() { // Arrange - var logger = new TestCommandOutputLogger(_testOutputHelper); + var logger = new TestConsole(); + logger.Width(500); string fakeProjectPath = "FakeProjectPath.csproj"; @@ -213,7 +233,7 @@ public async Task WhyCommand_InvalidProject_Fails() var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var errorOutput = logger.ShowErrors(); + var errorOutput = logger.Lines; Assert.Equal(ExitCodes.InvalidArguments, result); Assert.Contains($"Unable to run 'dotnet nuget why'. Missing or invalid path '{fakeProjectPath}'. Please provide a path to a project, solution file, or directory.", errorOutput); @@ -246,18 +266,21 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageCommandArgs, new MSBuildAPIUtility(logger)); + var console = new TestConsole(); + console.Width(500); + var whyCommandArgs = new WhyCommandArgs( project.ProjectPath, packageY.Id, [inputFrameworksOption, projectFramework], - logger, + console, CancellationToken.None); // Act var result = await WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var output = logger.ShowMessages(); + var output = console.Output; Assert.Equal(ExitCodes.Success, result); Assert.Contains($"The assets file '{project.AssetsFileOutputPath}' for project '{ProjectName}' does not contain a target for the specified input framework '{inputFrameworksOption}'.", output); 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 44a4e3379f5..33ad8d85daa 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 @@ -7,6 +7,7 @@ using FluentAssertions; using NuGet.CommandLine.XPlat.Commands; using NuGet.CommandLine.XPlat.Commands.Why; +using Spectre.Console.Testing; using Xunit; namespace NuGet.CommandLine.Xplat.Tests.Commands.Why @@ -20,7 +21,7 @@ public void WhyCommand_HasHelpUrl() Command rootCommand = new("nuget"); // Act - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance); + WhyCommand.Register(rootCommand, new TestConsole()); // Assert rootCommand.Subcommands[0].Should().BeAssignableTo(); @@ -33,7 +34,7 @@ public void WithTwoArguments_PathAndPackageAreSet() // Arrange Command rootCommand = new("nuget"); - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance, whyCommandArgs => + WhyCommand.Register(rootCommand, new TestConsole(), whyCommandArgs => { // Assert whyCommandArgs.Path.Should().Be(@"path\to\my.proj"); @@ -54,7 +55,7 @@ public void WithOneArguments_PackageIsSet() // Arrange Command rootCommand = new("nuget"); - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance, whyCommandArgs => + WhyCommand.Register(rootCommand, new TestConsole(), whyCommandArgs => { // Assert whyCommandArgs.Path.Should().NotBeNull(); @@ -75,7 +76,7 @@ public void WithZeroArguments_HasParseError() // Arrange Command rootCommand = new("nuget"); - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance, whyCommandArgs => + WhyCommand.Register(rootCommand, new TestConsole(), whyCommandArgs => { // Assert throw new Exception("Should not get here"); @@ -92,7 +93,7 @@ public void WithThreeArguments_HasParseError() // Arrange Command rootCommand = new("nuget"); - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance, whyCommandArgs => + WhyCommand.Register(rootCommand, new TestConsole(), whyCommandArgs => { // Assert throw new Exception("Should not get here"); @@ -112,7 +113,7 @@ public void FrameworkOption_CanBeAtAnyPosition(string args) // Arrange Command rootCommand = new("nuget"); - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance, whyCommandArgs => + WhyCommand.Register(rootCommand, new TestConsole(), whyCommandArgs => { // Assert whyCommandArgs.Path.Should().Be("my.proj"); @@ -135,7 +136,7 @@ public void FrameworkOption_CanBeLongOrShortForm(string arg) // Arrange Command rootCommand = new("nuget"); - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance, whyCommandArgs => + WhyCommand.Register(rootCommand, new TestConsole(), whyCommandArgs => { // Assert whyCommandArgs.Path.Should().Be("my.proj"); @@ -156,7 +157,7 @@ public void FrameworkOption_AcceptsMultipleValues() // Arrange Command rootCommand = new("nuget"); - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance, whyCommandArgs => + WhyCommand.Register(rootCommand, new TestConsole(), whyCommandArgs => { // Assert whyCommandArgs.Path.Should().Be("my.proj"); @@ -177,7 +178,7 @@ public void HelpOption_ShowsHelp() // Arrange Command rootCommand = new("nuget"); - WhyCommand.Register(rootCommand, NullLoggerWithColor.GetInstance, whyCommandArgs => + WhyCommand.Register(rootCommand, new TestConsole(), whyCommandArgs => { // Assert whyCommandArgs.Path.Should().Be("my.proj"); diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj index cebd25bcdb3..6a6b4be08e4 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/test/TestUtilities/Test.Utility/TestDotnetCLiUtility.cs b/test/TestUtilities/Test.Utility/TestDotnetCLiUtility.cs index e41aa95dfa1..9276ef549a8 100644 --- a/test/TestUtilities/Test.Utility/TestDotnetCLiUtility.cs +++ b/test/TestUtilities/Test.Utility/TestDotnetCLiUtility.cs @@ -196,6 +196,63 @@ private static void UpdateCliWithLatestNuGetAssemblies(string cliDirectory) CopyPackSdkArtifacts(artifactsDirectory, pathToSdkInCli, configuration); CopyRestoreArtifacts(artifactsDirectory, pathToSdkInCli, configuration); CopyNuGetSdkResolverArtifacts(artifactsDirectory, pathToSdkInCli, configuration); + AddSpectreConsoleToDepsJson(pathToSdkInCli); + } + + // Temporary. Can be removed once https://github.com/dotnet/dotnet/pull/3527 is merged and a new .NET SDK + private static void AddSpectreConsoleToDepsJson(string pathToSdkInCli) + { + string[] depsFiles = ["NuGet.CommandLine.XPlat.deps.json", "dotnet.deps.json"]; + + foreach (var depsFile in depsFiles) + { + var depsFilePath = Path.Combine(pathToSdkInCli, depsFile); + JObject jObject = JObject.Parse(File.ReadAllText(depsFilePath)); + + var targetNode = (JObject)((JObject)jObject["targets"]).Properties().First().Value; + JProperty spectreConsoleProperty = targetNode.Properties() + .FirstOrDefault(p => p.Name.StartsWith("Spectre.Console/")); + + bool changed = false; + + if (spectreConsoleProperty is null) + { + spectreConsoleProperty = new JProperty("Spectre.Console/0.54.0", JObject.Parse("""{"runtime":{"lib/net9.0/Spectre.Console.dll":{"assemblyVersion":"0.0.0.0","fileVersion":"0.54.0.0"}}}""")); + targetNode.Add(spectreConsoleProperty); + changed = true; + } + + JProperty library = (JProperty)((JObject)jObject["libraries"]).Properties() + .FirstOrDefault(p => p.Name.StartsWith("Spectre.Console/")); + if (library is null) + { + var value = JObject.Parse( + """ + { + "type": "package", + "serviceable": true, + "sha512": "sha512-StDXCFayfy0yB1xzUHT2tgEpV1/HFTiS4JgsAQS49EYTfMixSwwucaQs/bIOCwXjWwIQTMuxjUIxcB5XsJkFJA==", + "path": "spectre.console/0.54.0", + "hashPath": "spectre.console.0.54.0.nupkg.sha512" + } + """); + library = new JProperty("Spectre.Console/0.54.0", value); + ((JObject)jObject["libraries"]).Add(library); + changed = true; + } + + if (changed) + { + using (StreamWriter streamWriter = File.CreateText(depsFilePath)) + using (JsonTextWriter writer = new JsonTextWriter(streamWriter) + { + Formatting = Formatting.Indented + }) + { + jObject.WriteTo(writer); + } + } + } } private static void CopyRestoreArtifacts(string artifactsDirectory, string pathToSdkInCli, string configuration)