diff --git a/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Sln/List/DotNetSlnListerFixture.cs b/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Sln/List/DotNetSlnListerFixture.cs new file mode 100644 index 0000000000..0fbdbe7d85 --- /dev/null +++ b/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Sln/List/DotNetSlnListerFixture.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Cake.Common.Tools.DotNet.Sln.List; + +namespace Cake.Common.Tests.Fixtures.Tools.DotNet.Sln.List +{ + internal sealed class DotNetSlnListerFixture : DotNetFixture + { + public string Solution { get; set; } + + public string StandardError { get; set; } + + public IEnumerable Projects { get; private set; } + + public void GivenProjectsResult() + { + ProcessRunner.Process.SetStandardOutput(new string[] + { + "Project(s)", + "--------------------", + "Common\\Common.AspNetCore\\Common.AspNetCore.csproj", + "Common\\Common.Messaging\\Common.Messaging.csproj", + "Common\\Common.Utilities\\Common.Utilities.csproj" + }); + } + + public void GivenErrorResult() + { + ProcessRunner.Process.SetStandardError(new string[] + { + StandardError + }); + } + + protected override void RunTool() + { + var tool = new DotNetSlnLister(FileSystem, Environment, ProcessRunner, Tools); + Projects = tool.List(Solution, Settings); + } + } +} diff --git a/src/Cake.Common.Tests/Unit/Tools/DotNet/Sln/List/DotNetSlnListerTests.cs b/src/Cake.Common.Tests/Unit/Tools/DotNet/Sln/List/DotNetSlnListerTests.cs new file mode 100644 index 0000000000..c39a97a093 --- /dev/null +++ b/src/Cake.Common.Tests/Unit/Tools/DotNet/Sln/List/DotNetSlnListerTests.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Cake.Common.Tests.Fixtures.Tools.DotNet.Sln.List; +using Cake.Common.Tools.DotNet; +using Cake.Testing; +using Xunit; + +namespace Cake.Common.Tests.Unit.Tools.DotNet.Sln.List +{ + public sealed class DotNetSlnListerTests + { + public sealed class TheListMethod + { + [Fact] + public void Should_Throw_If_Process_Was_Not_Started() + { + // Given + var fixture = new DotNetSlnListerFixture(); + fixture.GivenProcessCannotStart(); + + // When + var result = Record.Exception(() => fixture.Run()); + + // Then + AssertEx.IsCakeException(result, ".NET CLI: Process was not started."); + } + + [Fact] + public void Should_Throw_If_Process_Has_A_Non_Zero_Exit_Code() + { + // Given + var fixture = new DotNetSlnListerFixture(); + fixture.GivenProcessExitsWithCode(1); + + // When + var result = Record.Exception(() => fixture.Run()); + + // Then + AssertEx.IsCakeException(result, ".NET CLI: Process returned an error (exit code 1)."); + } + + [Fact] + public void Should_Throw_If_Settings_Are_Null() + { + // Given + var fixture = new DotNetSlnListerFixture(); + fixture.Settings = null; + fixture.GivenDefaultToolDoNotExist(); + + // When + var result = Record.Exception(() => fixture.Run()); + + // Then + AssertEx.IsArgumentNullException(result, "settings"); + } + + [Fact] + public void Should_Not_Add_Solution_Argument() + { + // Given + var fixture = new DotNetSlnListerFixture(); + fixture.Solution = null; + + // When + var result = fixture.Run(); + + // Then + Assert.Equal("sln list", result.Args); + } + + [Fact] + public void Should_Add_Solution_Argument() + { + // Given + var fixture = new DotNetSlnListerFixture(); + fixture.Solution = "ToDo.sln"; + + // When + var result = fixture.Run(); + + // Then + Assert.Equal("sln \"/Working/ToDo.sln\" list", result.Args); + } + + [Fact] + public void Should_Add_Additional_Arguments() + { + // Given + var fixture = new DotNetSlnListerFixture(); + fixture.Solution = "ToDo.sln"; + fixture.Settings.Verbosity = DotNetVerbosity.Diagnostic; + + // When + var result = fixture.Run(); + + // Then + var expected = "sln \"/Working/ToDo.sln\" list --verbosity diagnostic"; + Assert.Equal(expected, result.Args); + } + + [Fact] + public void Should_Return_Correct_List_Of_Projects() + { + // Given + var fixture = new DotNetSlnListerFixture(); + fixture.GivenProjectsResult(); + + // When + var result = fixture.Run(); + + // Then + Assert.Collection(fixture.Projects, + item => + { + Assert.Equal(item, "Common\\Common.AspNetCore\\Common.AspNetCore.csproj"); + }, + item => + { + Assert.Equal(item, "Common\\Common.Messaging\\Common.Messaging.csproj"); + }, + item => + { + Assert.Equal(item, "Common\\Common.Utilities\\Common.Utilities.csproj"); + }); + } + + [Fact] + public void Should_Return_StandardError_ExitCode() + { + const string expectedStandardError = "Specified solution file C:\\Cake\\Cake.Core\\ does not exist, or there is no solution file in the directory."; + + // Given + var fixture = new DotNetSlnListerFixture(); + fixture.StandardError = expectedStandardError; + fixture.GivenErrorResult(); + + // When + fixture.Run(); + + // Then + Assert.Equal(expectedStandardError, fixture.StandardError); + } + } + } +} diff --git a/src/Cake.Common/Tools/DotNet/DotNetAliases.cs b/src/Cake.Common/Tools/DotNet/DotNetAliases.cs index 595e626d37..8a46139242 100644 --- a/src/Cake.Common/Tools/DotNet/DotNetAliases.cs +++ b/src/Cake.Common/Tools/DotNet/DotNetAliases.cs @@ -26,6 +26,7 @@ using Cake.Common.Tools.DotNet.Restore; using Cake.Common.Tools.DotNet.Run; using Cake.Common.Tools.DotNet.SDKCheck; +using Cake.Common.Tools.DotNet.Sln.List; using Cake.Common.Tools.DotNet.Test; using Cake.Common.Tools.DotNet.Tool; using Cake.Common.Tools.DotNet.VSTest; @@ -2883,5 +2884,93 @@ public static DotNetPackageList DotNetListPackage(this ICakeContext context, str var lister = new DotNetPackageLister(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools); return lister.List(project, settings); } + + /// + /// Lists all projects in a solution file. + /// + /// The context. + /// The list of projects. + /// + /// + /// var projects = DotNetSlnList(); + /// + /// foreach (var project in projects) + /// { + /// Information(project); + /// } + /// + /// + [CakeMethodAlias] + [CakeAliasCategory("Sln")] + [CakeNamespaceImport("Cake.Common.Tools.DotNet.Sln.List")] + public static IEnumerable DotNetSlnList(this ICakeContext context) + { + return context.DotNetSlnList(null); + } + + /// + /// Lists all projects in a solution file. + /// + /// The context. + /// The solution file to use. If this argument is omitted, the command searches the current directory for one. If it finds no solution file or multiple solution files, the command fails. + /// The list of projects. + /// + /// + /// var projects = DotNetSlnList("./app/app.sln"); + /// + /// foreach (var project in projects) + /// { + /// Information(project); + /// } + /// + /// + [CakeMethodAlias] + [CakeAliasCategory("Sln")] + [CakeNamespaceImport("Cake.Common.Tools.DotNet.Sln.List")] + public static IEnumerable DotNetSlnList(this ICakeContext context, FilePath solution) + { + return context.DotNetSlnList(solution, null); + } + + /// + /// Lists all projects in a solution file. + /// + /// The context. + /// The solution file to use. If this argument is omitted, the command searches the current directory for one. If it finds no solution file or multiple solution files, the command fails. + /// The settings. + /// The list of projects. + /// + /// + /// var settings = new DotNetSlnListSettings + /// { + /// Verbosity = DotNetVerbosity.Diagnostic + /// }; + /// + /// var projects = DotNetSlnList("./app/app.sln"); + /// + /// foreach (var project in projects) + /// { + /// Information(project); + /// } + /// + /// + [CakeMethodAlias] + [CakeAliasCategory("Sln")] + [CakeNamespaceImport("Cake.Common.Tools.DotNet.Sln.List")] + public static IEnumerable DotNetSlnList(this ICakeContext context, FilePath solution, DotNetSlnListSettings settings) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (settings is null) + { + settings = new DotNetSlnListSettings(); + } + + var lister = new DotNetSlnLister(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools); + return lister.List(solution, settings); + } } } diff --git a/src/Cake.Common/Tools/DotNet/Sln/List/DotNetSlnListSettings.cs b/src/Cake.Common/Tools/DotNet/Sln/List/DotNetSlnListSettings.cs new file mode 100644 index 0000000000..62a11de27f --- /dev/null +++ b/src/Cake.Common/Tools/DotNet/Sln/List/DotNetSlnListSettings.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Cake.Common.Tools.DotNet.Sln.List +{ + /// + /// Contains settings used by . + /// + public sealed class DotNetSlnListSettings : DotNetSettings + { + } +} diff --git a/src/Cake.Common/Tools/DotNet/Sln/List/DotNetSlnLister.cs b/src/Cake.Common/Tools/DotNet/Sln/List/DotNetSlnLister.cs new file mode 100644 index 0000000000..cb60cab9cf --- /dev/null +++ b/src/Cake.Common/Tools/DotNet/Sln/List/DotNetSlnLister.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Cake.Core; +using Cake.Core.IO; +using Cake.Core.Tooling; + +namespace Cake.Common.Tools.DotNet.Sln.List +{ + /// + /// .NET projects lister. + /// + public sealed class DotNetSlnLister : DotNetTool + { + private readonly ICakeEnvironment _environment; + + /// + /// Initializes a new instance of the class. + /// + /// The file system. + /// The environment. + /// The process runner. + /// The tool locator. + public DotNetSlnLister( + IFileSystem fileSystem, + ICakeEnvironment environment, + IProcessRunner processRunner, + IToolLocator tools) : base(fileSystem, environment, processRunner, tools) + { + _environment = environment; + } + + /// + /// Lists all projects in a solution file. + /// + /// The solution file to use. If not specified, the command searches the current directory for one. If it finds no solution file or multiple solution files, the command fails. + /// The settings. + /// The list of project-to-project references. + public IEnumerable List(FilePath solution, DotNetSlnListSettings settings) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + var processSettings = new ProcessSettings + { + RedirectStandardOutput = true + }; + + IEnumerable result = null; + RunCommand(settings, GetArguments(solution, settings), processSettings, + process => result = process.GetStandardOutput()); + + return ParseResult(result).ToList(); + } + + private ProcessArgumentBuilder GetArguments(FilePath solution, DotNetSlnListSettings settings) + { + var builder = CreateArgumentBuilder(settings); + + builder.Append("sln"); + + // Solution path + if (solution != null) + { + builder.AppendQuoted(solution.MakeAbsolute(_environment).FullPath); + } + + builder.Append("list"); + + return builder; + } + + private static IEnumerable ParseResult(IEnumerable result) + { + bool first = true; + foreach (var line in result) + { + if (first) + { + if (line?.StartsWith("Project(s)") == true) + { + first = false; + } + continue; + } + + if (string.IsNullOrWhiteSpace(line)) + { + break; + } + + var trimmedLine = line.Trim(); + + if (trimmedLine.Trim().All(c => c == '-')) + { + continue; + } + + yield return trimmedLine; + } + } + } +} diff --git a/tests/integration/Cake.Common/Tools/DotNet/DotNetAliases.cake b/tests/integration/Cake.Common/Tools/DotNet/DotNetAliases.cake index 0fd9f3a0ce..711ef50aae 100644 --- a/tests/integration/Cake.Common/Tools/DotNet/DotNetAliases.cake +++ b/tests/integration/Cake.Common/Tools/DotNet/DotNetAliases.cake @@ -437,6 +437,20 @@ Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetListPackage") Assert.Contains(result.Projects, item => item.Path == project); }); +Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetSlnList") + .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.Setup") + .Does(() => +{ + // Given + var path = Paths.Temp.Combine("./Cake.Common/Tools/DotNet"); + var solution = path.CombineWithFilePath("hwapp.sln"); + var project = new DirectoryPath("./hwapp.common").CombineWithFilePath("hwapp.common.csproj"); + // When + var result = DotNetSlnList(solution.FullPath); + // Then + Assert.Contains(result, item => item == project); +}); + Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetBuildServerShutdown") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetRestore") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetBuild") @@ -465,6 +479,7 @@ Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetBuildServerShutdown") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetAddReference") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetRemoveReference") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetListReference") + .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetSlnList") .Does(() => { // When