diff --git a/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Workload/List/DotNetWorkloadListerFixture.cs b/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Workload/List/DotNetWorkloadListerFixture.cs new file mode 100644 index 0000000000..241002d69e --- /dev/null +++ b/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Workload/List/DotNetWorkloadListerFixture.cs @@ -0,0 +1,45 @@ +// 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.Workload.List; + +namespace Cake.Common.Tests.Fixtures.Tools.DotNet.Workload.List +{ + internal sealed class DotNetWorkloadListerFixture : DotNetFixture + { + public IEnumerable Workloads { get; set; } + + public void GivenInstalledWorkloadsResult() + { + ProcessRunner.Process.SetStandardOutput(new string[] + { + "Installed Workload Ids Manifest Version Installation Source", + "--------------------------------------------------------------------------------------", + "maui-ios 6.0.312/6.0.300 VS 17.3.32804.467, VS 17.4.32804.182", + "maui-windows 6.0.312/6.0.300 VS 17.3.32804.467, VS 17.4.32804.182", + "android 32.0.301/6.0.300 VS 17.3.32804.467, VS 17.4.32804.182", + "", + "Use `dotnet workload search` to find additional workloads to install." + }); + } + + public void GivenEmptyInstalledWorkloadsResult() + { + ProcessRunner.Process.SetStandardOutput(new string[] + { + "Installed Workload Ids Manifest Version Installation Source", + "---------------------------------------------------------------------", + "", + "Use `dotnet workload search` to find additional workloads to install." + }); + } + + protected override void RunTool() + { + var tool = new DotNetWorkloadLister(FileSystem, Environment, ProcessRunner, Tools); + Workloads = tool.List(Settings); + } + } +} diff --git a/src/Cake.Common.Tests/Unit/Tools/DotNet/Workload/List/DotNetWorkloadListTests.cs b/src/Cake.Common.Tests/Unit/Tools/DotNet/Workload/List/DotNetWorkloadListTests.cs new file mode 100644 index 0000000000..4dd79f8dbd --- /dev/null +++ b/src/Cake.Common.Tests/Unit/Tools/DotNet/Workload/List/DotNetWorkloadListTests.cs @@ -0,0 +1,119 @@ +// 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.Workload.List; +using Cake.Testing; +using Xunit; + +namespace Cake.Common.Tests.Unit.Tools.DotNet.Workload.List +{ + public sealed class DotNetWorkloadListTests + { + public sealed class TheWorkloadListMethod + { + [Fact] + public void Should_Throw_If_Process_Was_Not_Started() + { + // Given + var fixture = new DotNetWorkloadListerFixture(); + 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 DotNetWorkloadListerFixture(); + 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 DotNetWorkloadListerFixture(); + fixture.Settings = null; + fixture.GivenDefaultToolDoNotExist(); + + // When + var result = Record.Exception(() => fixture.Run()); + + // Then + AssertEx.IsArgumentNullException(result, "settings"); + } + + [Fact] + public void Should_Add_Verbosity_Argument() + { + // Given + var fixture = new DotNetWorkloadListerFixture(); + fixture.Settings.Verbosity = Common.Tools.DotNet.DotNetVerbosity.Normal; + + // When + var result = fixture.Run(); + + // Then + Assert.Equal("workload list --verbosity normal", result.Args); + } + + [Fact] + public void Should_Return_Correct_List_Of_Workloads() + { + // Given + var fixture = new DotNetWorkloadListerFixture(); + fixture.GivenInstalledWorkloadsResult(); + + // When + var result = fixture.Run(); + + // Then + Assert.Collection(fixture.Workloads, + item => + { + Assert.Equal(item.Id, "maui-ios"); + Assert.Equal(item.ManifestVersion, "6.0.312/6.0.300"); + Assert.Equal(item.InstallationSource, "VS 17.3.32804.467, VS 17.4.32804.182"); + }, + item => + { + Assert.Equal(item.Id, "maui-windows"); + Assert.Equal(item.ManifestVersion, "6.0.312/6.0.300"); + Assert.Equal(item.InstallationSource, "VS 17.3.32804.467, VS 17.4.32804.182"); + }, + item => + { + Assert.Equal(item.Id, "android"); + Assert.Equal(item.ManifestVersion, "32.0.301/6.0.300"); + Assert.Equal(item.InstallationSource, "VS 17.3.32804.467, VS 17.4.32804.182"); + }); + } + + [Fact] + public void Should_Return_Empty_List_Of_Workloads() + { + // Given + var fixture = new DotNetWorkloadListerFixture(); + fixture.GivenEmptyInstalledWorkloadsResult(); + + // When + var result = fixture.Run(); + + // Then + Assert.Empty(fixture.Workloads); + } + } + } +} diff --git a/src/Cake.Common/Tools/DotNet/DotNetAliases.cs b/src/Cake.Common/Tools/DotNet/DotNetAliases.cs index b335c567cf..6f945837a8 100644 --- a/src/Cake.Common/Tools/DotNet/DotNetAliases.cs +++ b/src/Cake.Common/Tools/DotNet/DotNetAliases.cs @@ -23,6 +23,7 @@ using Cake.Common.Tools.DotNet.Tool; using Cake.Common.Tools.DotNet.VSTest; using Cake.Common.Tools.DotNet.Workload.Install; +using Cake.Common.Tools.DotNet.Workload.List; using Cake.Common.Tools.DotNet.Workload.Search; using Cake.Common.Tools.DotNet.Workload.Uninstall; using Cake.Common.Tools.DotNetCore.Build; @@ -182,7 +183,7 @@ public static void DotNetRestore(this ICakeContext context, string root) /// Sources = new[] {"https://www.example.com/nugetfeed", "https://www.example.com/nugetfeed2"}, /// FallbackSources = new[] {"https://www.example.com/fallbacknugetfeed"}, /// PackagesDirectory = "./packages", - /// Verbosity = Information, + /// DotNetVerbosity.Information, /// DisableParallel = true, /// InferRuntimes = new[] {"runtime1", "runtime2"} /// }; @@ -211,7 +212,7 @@ public static void DotNetRestore(this ICakeContext context, DotNetRestoreSetting /// Sources = new[] {"https://www.example.com/nugetfeed", "https://www.example.com/nugetfeed2"}, /// FallbackSources = new[] {"https://www.example.com/fallbacknugetfeed"}, /// PackagesDirectory = "./packages", - /// Verbosity = Information, + /// DotNetVerbosity.Information, /// DisableParallel = true, /// InferRuntimes = new[] {"runtime1", "runtime2"} /// }; @@ -1907,7 +1908,7 @@ public static IEnumerable DotNetWorkloadSearch(this ICakeContext /// /// var settings = new DotNetWorkloadSearchSettings /// { - /// Verbosity = Detailed + /// DotNetVerbosity.Detailed /// }; /// /// var workloads = DotNetWorkloadSearch("maui", settings); @@ -2075,5 +2076,68 @@ public static void DotNetWorkloadInstall(this ICakeContext context, IEnumerable< var installer = new DotNetWorkloadInstaller(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools); installer.Install(workloadIds, settings); } + + /// + /// Lists all installed workloads. + /// + /// The context. + /// The list of installed workloads. + /// + /// + /// var workloadIds = DotNetWorkloadList(); + /// + /// foreach (var workloadId in workloadIds) + /// { + /// Information($"Installed Workload Id: {workloadId}"); + /// } + /// + /// + [CakeMethodAlias] + [CakeAliasCategory("Workload")] + [CakeNamespaceImport("Cake.Common.Tools.DotNet.Workload.List")] + public static IEnumerable DotNetWorkloadList(this ICakeContext context) + { + return context.DotNetWorkloadList(null); + } + + /// + /// Lists all installed workloads. + /// + /// The context. + /// The settings. + /// The list of installed workloads. + /// + /// + /// var settings = new DotNetWorkloadListSettings + /// { + /// Verbosity = DotNetVerbosity.Detailed + /// }; + /// + /// var workloads = DotNetWorkloadList(settings); + /// + /// foreach (var workload in workloads) + /// { + /// Information($"Installed Workload Id: {workload.Id}\t Manifest Version: {workload.ManifestVersion}\t Installation Source: {workload.InstallationSource}"); + /// } + /// + /// + [CakeMethodAlias] + [CakeAliasCategory("Workload")] + [CakeNamespaceImport("Cake.Common.Tools.DotNet.Workload.List")] + public static IEnumerable DotNetWorkloadList(this ICakeContext context, DotNetWorkloadListSettings settings) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (settings == null) + { + settings = new DotNetWorkloadListSettings(); + } + + var lister = new DotNetWorkloadLister(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools); + return lister.List(settings); + } } } diff --git a/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadListItem.cs b/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadListItem.cs new file mode 100644 index 0000000000..b572100154 --- /dev/null +++ b/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadListItem.cs @@ -0,0 +1,40 @@ +// 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.Workload.List +{ + /// + /// An item as returned by . + /// + public sealed class DotNetWorkloadListItem + { + /// + /// Initializes a new instance of the class. + /// + /// The workload Id. + /// The workload manifest version. + /// The workload installation source. + public DotNetWorkloadListItem(string id, string manifestVersion, string installationSource) + { + Id = id; + ManifestVersion = manifestVersion; + InstallationSource = installationSource; + } + + /// + /// Gets the workload ID. + /// + public string Id { get; } + + /// + /// Gets the manifest version of the workload as string. + /// + public string ManifestVersion { get; } + + /// + /// Gets the installation source of the workload as string. + /// + public string InstallationSource { get; } + } +} diff --git a/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadListSettings.cs b/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadListSettings.cs new file mode 100644 index 0000000000..daef0e0ea5 --- /dev/null +++ b/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadListSettings.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.Workload.List +{ + /// + /// Contains settings used by . + /// + public sealed class DotNetWorkloadListSettings : DotNetSettings + { + } +} diff --git a/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadLister.cs b/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadLister.cs new file mode 100644 index 0000000000..5b4471f3f6 --- /dev/null +++ b/src/Cake.Common/Tools/DotNet/Workload/List/DotNetWorkloadLister.cs @@ -0,0 +1,104 @@ +// 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.Workload.List +{ + /// + /// .NET workloads lister. + /// + public sealed class DotNetWorkloadLister : DotNetTool + { + /// + /// Initializes a new instance of the class. + /// + /// The file system. + /// The environment. + /// The process runner. + /// The tool locator. + public DotNetWorkloadLister( + IFileSystem fileSystem, + ICakeEnvironment environment, + IProcessRunner processRunner, + IToolLocator tools) : base(fileSystem, environment, processRunner, tools) + { + } + + /// + /// Lists all installed workloads. + /// + /// The settings. + /// The list of installed workloads. + public IEnumerable List(DotNetWorkloadListSettings settings) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + var processSettings = new ProcessSettings + { + RedirectStandardOutput = true + }; + + IEnumerable result = null; + RunCommand(settings, GetArguments(settings), processSettings, + process => result = process.GetStandardOutput()); + + return ParseResult(result).ToList(); + } + + private ProcessArgumentBuilder GetArguments(DotNetWorkloadListSettings settings) + { + var builder = CreateArgumentBuilder(settings); + + builder.Append("workload list"); + + return builder; + } + + private static IEnumerable ParseResult(IEnumerable result) + { + bool first = true; + int manifestIndex = -1; + int sourceIndex = -1; + foreach (var line in result) + { + if (first) + { + if (line?.StartsWith("Installed Workload Ids") == true + && (manifestIndex = line?.IndexOf("Manifest Version") ?? -1) > 22 + && (sourceIndex = line?.IndexOf("Installation Source") ?? -1) > 39) + { + first = false; + } + continue; + } + + if (string.IsNullOrWhiteSpace(line)) + { + break; + } + + var trimmedLine = line.Trim(); + + if (trimmedLine.Trim().All(c => c == '-')) + { + continue; + } + + yield return new DotNetWorkloadListItem( + string.Concat(trimmedLine.Take(manifestIndex)).TrimEnd(), + string.Concat(trimmedLine.Take(sourceIndex).Skip(manifestIndex)).TrimEnd(), + string.Concat(trimmedLine.Skip(sourceIndex))); + } + } + } +}