winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 9b163cd5bcd60396c5b702b6867c5bda9cec07f1
parent a5bffb2a86a62597a2d2789bdf9fe79bd5f7fc29
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Thu, 14 Sep 2023 14:29:29 -0700

Add new cmdlets and cancellation support for Microsoft.WinGet.Configuration (#3614)

Test-WinGetConfiguration to test a configuration. Equivalent of winget configure test
Confirm-WinGetConfiguration to validate a configuration. Equivalent of winget configure validate except it just calls the configuration APIs without the extra validation winget does.
Support cancellation via CTRL-C for blocking cmdlets and Stop-WinGetConfiguration for configuration stated with Start-WinGetConfiguration
Add tests for Test-WinGetConfiguration and Confirm-WinGetConfiguration cmdlets.
Add more tests for Open-WinGetConfiguration
Cancellation verified manually
Diffstat:
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs | 19++++++++++++++++---
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConfirmWinGetConfigurationCmdlet.cs | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs | 17+++++++++++++++--
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs | 20++++++++++++++++----
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs | 2+-
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StopWinGetConfigurationCmdlet.cs | 39+++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/TestWinGetConfigurationCmdlet.cs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs | 20+++++++++++++++++---
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs | 129++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs | 14+++++++-------
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/IAsyncOperationExtensions.cs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/TestConfigurationSetProgressOutput.cs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs | 18++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationTestResult.cs | 39+++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationSetResult.cs | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationUnitResult.cs | 32++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationSetResult.cs | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationUnitResult.cs | 25+++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs | 13+++++++++++--
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx | 5++++-
Msrc/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 | 3+++
Msrc/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 | 197++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
22 files changed, 902 insertions(+), 34 deletions(-)

diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs @@ -7,7 +7,6 @@ namespace Microsoft.WinGet.Configuration.Cmdlets { using System.Management.Automation; - using System.Threading; using Microsoft.WinGet.Configuration.Engine.Commands; using Microsoft.WinGet.Configuration.Engine.PSObjects; @@ -19,6 +18,8 @@ namespace Microsoft.WinGet.Configuration.Cmdlets [Cmdlet(VerbsLifecycle.Complete, "WinGetConfiguration")] public sealed class CompleteWinGetConfigurationCmdlet : PSCmdlet { + private ConfigurationCommand runningCommand = null; + /// <summary> /// Gets or sets the configuration task. /// </summary> @@ -34,8 +35,20 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - var configCommand = new ConfigurationCommand(this); - configCommand.Continue(this.ConfigurationJob); + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.Continue(this.ConfigurationJob); + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(this.ConfigurationJob); + this.runningCommand.Cancel(); + } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConfirmWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConfirmWinGetConfigurationCmdlet.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfirmWinGetConfigurationCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// <summary> + /// Confirm-WinGetConfiguration + /// Validates winget configuration. + /// </summary> + [Cmdlet(VerbsLifecycle.Confirm, "WinGetConfiguration")] + public class ConfirmWinGetConfigurationCmdlet : PSCmdlet + { + private ConfigurationCommand runningCommand = null; + + /// <summary> + /// Gets or sets the configuration set. + /// </summary> + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// <summary> + /// Validate configuration. + /// </summary> + protected override void ProcessRecord() + { + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.Validate(this.Set); + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs @@ -18,6 +18,8 @@ namespace Microsoft.WinGet.Configuration.Cmdlets [Cmdlet(VerbsCommon.Get, "WinGetConfigurationDetails")] public sealed class GetWinGetConfigurationDetailsCmdlet : PSCmdlet { + private ConfigurationCommand runningCommand = null; + /// <summary> /// Gets or sets the configuration set. /// </summary> @@ -33,8 +35,19 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void ProcessRecord() { - var configCommand = new ConfigurationCommand(this); - configCommand.GetDetails(this.Set); + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.GetDetails(this.Set); + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(); + } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs @@ -6,8 +6,8 @@ namespace Microsoft.WinGet.Configuration.Cmdlets { + using System; using System.Management.Automation; - using System.Threading; using Microsoft.WinGet.Configuration.Engine.Commands; using Microsoft.WinGet.Configuration.Engine.PSObjects; @@ -20,6 +20,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets public sealed class InvokeWinGetConfigurationCmdlet : PSCmdlet { private bool acceptedAgreements = false; + private ConfigurationCommand runningCommand = null; /// <summary> /// Gets or sets the configuration set. @@ -42,7 +43,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void BeginProcessing() { - this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool()); + this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool(), true); } /// <summary> @@ -52,8 +53,19 @@ namespace Microsoft.WinGet.Configuration.Cmdlets { if (this.acceptedAgreements) { - var configCommand = new ConfigurationCommand(this); - configCommand.Apply(this.Set); + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.Apply(this.Set); + } + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs @@ -41,7 +41,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// </summary> protected override void BeginProcessing() { - this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool()); + this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool(), true); } /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StopWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StopWinGetConfigurationCmdlet.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="StopWinGetConfigurationCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// <summary> + /// Stop-WinGetConfiguration. + /// Cancels a configuration previously started by Start-WinGetConfiguration. + /// </summary> + [Cmdlet(VerbsLifecycle.Stop, "WinGetConfiguration")] + public sealed class StopWinGetConfigurationCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the configuration task. + /// </summary> + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationJob ConfigurationJob { get; set; } + + /// <summary> + /// Starts to apply the configuration and wait for it to complete. + /// </summary> + protected override void ProcessRecord() + { + var configCommand = new ConfigurationCommand(this); + configCommand.Cancel(this.ConfigurationJob); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/TestWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/TestWinGetConfigurationCmdlet.cs @@ -0,0 +1,70 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestWinGetConfigurationCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// <summary> + /// Test-WinGetConfiguration + /// Tests configuration. + /// </summary> + [Cmdlet(VerbsDiagnostic.Test, "WinGetConfiguration")] + public class TestWinGetConfigurationCmdlet : PSCmdlet + { + private bool acceptedAgreements = false; + private ConfigurationCommand runningCommand = null; + + /// <summary> + /// Gets or sets the configuration set. + /// </summary> + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to accept the configuration agreements. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AcceptConfigurationAgreements { get; set; } + + /// <summary> + /// Pre-processing operations. + /// </summary> + protected override void BeginProcessing() + { + this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool(), false); + } + + /// <summary> + /// Test configuration. + /// </summary> + protected override void ProcessRecord() + { + if (this.acceptedAgreements) + { + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.Test(this.Set); + } + } + + /// <summary> + /// Interrupts currently running code within the command. + /// </summary> + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs @@ -32,8 +32,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands private readonly Thread originalThread; private readonly CancellationTokenSource source = new (); - - private CancellationToken cancellationToken; private BlockingCollection<QueuedStream> queuedStreams = new (); private int progressActivityId = 0; @@ -73,7 +71,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands this.PsCmdlet = psCmdlet; this.originalThread = Thread.CurrentThread; - this.cancellationToken = this.source.Token; } /// <summary> @@ -123,6 +120,14 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands protected PSCmdlet PsCmdlet { get; private set; } /// <summary> + /// Request cancellation for this command. + /// </summary> + public void Cancel() + { + this.source.Cancel(); + } + + /// <summary> /// Complete this operation. /// </summary> public virtual void Complete() @@ -378,6 +383,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands return Interlocked.Increment(ref this.progressActivityId); } + /// <summary> + /// Gets the cancellation token. + /// </summary> + /// <returns>CancellationToken.</returns> + protected CancellationToken GetCancellationToken() + { + return this.source.Token; + } + private void CmdletWrite(StreamType streamType, object data, AsyncCommand writeCommand) { switch (streamType) diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs @@ -41,15 +41,17 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// </summary> /// <param name="psCmdlet">PSCmdlet.</param> /// <param name="hasAccepted">Has already accepted.</param> + /// <param name="isApply">If prompt is for apply.</param> /// <returns>If accepted.</returns> - public static bool ConfirmConfigurationProcessing(PSCmdlet psCmdlet, bool hasAccepted) + public static bool ConfirmConfigurationProcessing(PSCmdlet psCmdlet, bool hasAccepted, bool isApply) { bool result = false; if (!hasAccepted) { + var prompt = isApply ? Resources.ConfigurationWarningPromptApply : Resources.ConfigurationWarningPromptTest; bool yesToAll = false; bool noToAll = false; - result = psCmdlet.ShouldContinue(Resources.ConfigurationWarningPrompt, Resources.ConfigurationWarning, true, ref yesToAll, ref noToAll); + result = psCmdlet.ShouldContinue(prompt, Resources.ConfigurationWarning, true, ref yesToAll, ref noToAll); if (yesToAll) { @@ -205,6 +207,78 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands this.ContinueHelper(psConfigurationJob); } + /// <summary> + /// Test configuration. + /// </summary> + /// <param name="psConfigurationSet">PSConfigurationSet.</param> + public void Test(PSConfigurationSet psConfigurationSet) + { + psConfigurationSet.PsProcessor.UpdateDiagnosticCmdlet(this); + + if (!psConfigurationSet.CanProcess()) + { + throw new InvalidOperationException(); + } + + var runningTask = this.RunOnMTA<PSTestConfigurationSetResult>( + async () => + { + try + { + return await this.TestConfigurationAsync(psConfigurationSet); + } + finally + { + this.Complete(); + psConfigurationSet.DoneProcessing(); + } + }); + + this.Wait(runningTask); + this.Write(StreamType.Object, runningTask.Result); + } + + /// <summary> + /// Validates configuration. + /// </summary> + /// <param name="psConfigurationSet">PSConfigurationSet.</param> + public void Validate(PSConfigurationSet psConfigurationSet) + { + psConfigurationSet.PsProcessor.UpdateDiagnosticCmdlet(this); + + if (!psConfigurationSet.CanProcess()) + { + throw new InvalidOperationException(); + } + + var runningTask = this.RunOnMTA<PSValidateConfigurationSetResult>( + async () => + { + try + { + var setResult = await this.ApplyConfigurationAsync(psConfigurationSet, ApplyConfigurationSetFlags.PerformConsistencyCheckOnly); + return new PSValidateConfigurationSetResult(setResult); + } + finally + { + this.Complete(); + psConfigurationSet.DoneProcessing(); + } + }); + + this.Wait(runningTask); + this.Write(StreamType.Object, runningTask.Result); + } + + /// <summary> + /// Cancels a configuration job. + /// </summary> + /// <param name="psConfigurationJob">PSConfiguration job.</param> + public void Cancel(PSConfigurationJob psConfigurationJob) + { + psConfigurationJob.StartCommand.Cancel(); + } + private void ContinueHelper(PSConfigurationJob psConfigurationJob) { // Signal the command that it can write to streams and wait for task. @@ -264,7 +338,9 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { try { - return await this.ApplyConfigurationAsync(psConfigurationSet); + var setResult = await this.ApplyConfigurationAsync(psConfigurationSet, ApplyConfigurationSetFlags.None); + psConfigurationSet.ApplyCompleted = true; + return new PSApplyConfigurationSetResult(setResult); } finally { @@ -276,7 +352,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands return new PSConfigurationJob(runningTask, this); } - private async Task<PSApplyConfigurationSetResult> ApplyConfigurationAsync(PSConfigurationSet psConfigurationSet) + private async Task<ApplyConfigurationSetResult> ApplyConfigurationAsync(PSConfigurationSet psConfigurationSet, ApplyConfigurationSetFlags flags) { if (!psConfigurationSet.HasDetails) { @@ -295,20 +371,53 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands Resources.OperationCompleted, set.Units.Count); - var applyTask = processor.ApplySetAsync(set, ApplyConfigurationSetFlags.None); + var applyTask = processor.ApplySetAsync(set, flags); applyTask.Progress = applyProgressOutput.Progress; try { - var result = await applyTask; + var result = await applyTask.AsTask(this.GetCancellationToken()); applyProgressOutput.HandleProgress(result); - - return new PSApplyConfigurationSetResult(result); + return result; } finally { applyProgressOutput.CompleteProgress(); - psConfigurationSet.ApplyCompleted = true; + } + } + + private async Task<PSTestConfigurationSetResult> TestConfigurationAsync(PSConfigurationSet psConfigurationSet) + { + if (!psConfigurationSet.HasDetails) + { + this.Write(StreamType.Verbose, "Getting details for configuration set"); + await this.GetSetDetailsAsync(psConfigurationSet, true); + } + + var processor = psConfigurationSet.PsProcessor.Processor; + var set = psConfigurationSet.Set; + + var testProgressOutput = new TestConfigurationSetProgressOutput( + this, + this.GetNewProgressActivityId(), + Resources.ConfigurationAssert, + Resources.OperationInProgress, + Resources.OperationCompleted, + set.Units.Count); + + var testTask = processor.TestSetAsync(set); + testTask.Progress = testProgressOutput.Progress; + + try + { + var result = await testTask.AsTask(this.GetCancellationToken()); + testProgressOutput.HandleProgress(result); + + return new PSTestConfigurationSetResult(result); + } + finally + { + testProgressOutput.CompleteProgress(); } } @@ -339,7 +448,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands try { - var result = await detailsTask; + var result = await detailsTask.AsTask(this.GetCancellationToken()); detailsProgressOutput.HandleProgress(result); if (result.UnitResults.Where(u => u.ResultInformation.ResultCode != null).Any()) diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs @@ -29,32 +29,32 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions private static string GetMessage(OpenConfigurationSetResult openResult, string configurationFile) { var sb = new StringBuilder(); - sb.AppendLine($"Failed to open configuration set at {configurationFile} with error 0x{openResult.ResultCode.HResult:X}"); + sb.Append($"Failed to open configuration set at {configurationFile} with error 0x{openResult.ResultCode.HResult:X} "); switch (openResult.ResultCode.HResult) { case ErrorCodes.WingetConfigErrorInvalidFieldType: - sb.AppendLine(string.Format(Resources.ConfigurationFieldInvalidType, openResult.Field)); + sb.Append(string.Format(Resources.ConfigurationFieldInvalidType, openResult.Field)); break; case ErrorCodes.WingetConfigErrorInvalidFieldValue: - sb.AppendLine(string.Format(Resources.ConfigurationFieldInvalidValue, openResult.Field, openResult.Value)); + sb.Append(string.Format(Resources.ConfigurationFieldInvalidValue, openResult.Field, openResult.Value)); break; case ErrorCodes.WingetConfigErrorMissingField: - sb.AppendLine(string.Format(Resources.ConfigurationFieldMissing, openResult.Field)); + sb.Append(string.Format(Resources.ConfigurationFieldMissing, openResult.Field)); break; case ErrorCodes.WingetConfigErrorUnknownConfigurationFileVersion: - sb.AppendLine(string.Format(Resources.ConfigurationFileVersionUnknown, openResult.Value)); + sb.Append(string.Format(Resources.ConfigurationFileVersionUnknown, openResult.Value)); break; case ErrorCodes.WingetConfigErrorInvalidConfigurationFile: case ErrorCodes.WingetConfigErrorInvalidYaml: default: - sb.AppendLine(Resources.ConfigurationFileInvalid); + sb.Append(Resources.ConfigurationFileInvalid); break; } if (openResult.Line != 0) { - sb.AppendLine(string.Format(Resources.SeeLineAndColumn, openResult.Line, openResult.Column)); + sb.Append($" {string.Format(Resources.SeeLineAndColumn, openResult.Line, openResult.Column)}"); } return sb.ToString(); diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/IAsyncOperationExtensions.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/IAsyncOperationExtensions.cs @@ -0,0 +1,74 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IAsyncOperationExtensions.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Extensions +{ + using System.Threading; + using System.Threading.Tasks; + using Windows.Foundation; + + /// <summary> + /// Extension methods for IAsyncOperation objects. + /// </summary> + internal static class IAsyncOperationExtensions + { + /// <summary> + /// Wrap IAsyncOperationWithProgress into a task with cancellation support. + /// </summary> + /// <typeparam name="TOperationResult">The result of the operation.</typeparam> + /// <typeparam name="TProgressData">The progress data of the operation.</typeparam> + /// <param name="asyncOperation">The async operation.</param> + /// <param name="cancellationToken">Optional cancellation token.</param> + /// <returns>A task.</returns> + public static Task<TOperationResult> AsTask<TOperationResult, TProgressData>(this IAsyncOperationWithProgress<TOperationResult, TProgressData> asyncOperation, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource<TOperationResult>(); + if (cancellationToken != default) + { + cancellationToken.Register(asyncOperation.Cancel); + } + + asyncOperation.Completed = (asyncInfo, asyncStatus) => + { + switch (asyncStatus) + { + case AsyncStatus.Canceled: + tcs.SetCanceled(); + break; + case AsyncStatus.Completed: + tcs.SetResult(asyncInfo.GetResults()); + break; + case AsyncStatus.Error: + tcs.SetException(asyncInfo.ErrorCode); + break; + case AsyncStatus.Started: + break; + default: + break; + } + }; + + // Make sure to throw operation cancelled exception if needed. + return tcs.Task.ContinueWith( + t => + { + if (t.IsCanceled) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + if (!t.IsFaulted) + { + return t.Result; + } + + // If IsFaulted is true, the task's Status is equal to Faulted, + // and its Exception property will be non-null. + throw t.Exception!; + }); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/TestConfigurationSetProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/TestConfigurationSetProgressOutput.cs @@ -0,0 +1,58 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestConfigurationSetProgressOutput.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Helpers +{ + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Windows.Foundation; + + /// <summary> + /// Helper to handle progress callbacks for TestSetAsync. + /// </summary> + internal class TestConfigurationSetProgressOutput : ConfigurationSetProgressOutputBase<TestConfigurationSetResult, TestConfigurationUnitResult> + { + private bool isFirstProgress = true; + + /// <summary> + /// Initializes a new instance of the <see cref="TestConfigurationSetProgressOutput"/> class. + /// </summary> + /// <param name="cmd">Command that outputs the messages.</param> + /// <param name="activityId">The activity id of the progress bar.</param> + /// <param name="activity">The activity.</param> + /// <param name="inProgressMessage">The message in the progress bar.</param> + /// <param name="completeMessage">The activity complete message.</param> + /// <param name="totalUnitsExpected">Total of units expected.</param> + public TestConfigurationSetProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) + { + } + + /// <inheritdoc/> + public override void Progress(IAsyncOperationWithProgress<TestConfigurationSetResult, TestConfigurationUnitResult> operation, TestConfigurationUnitResult data) + { + if (this.isFirstProgress) + { + this.HandleProgress(operation.GetResults()); + } + + this.CompleteUnit(data.Unit); + } + + /// <inheritdoc/> + public override void HandleProgress(TestConfigurationSetResult result) + { + if (!this.isFirstProgress) + { + this.isFirstProgress = false; + foreach (var unitResult in result.UnitResults) + { + this.CompleteUnit(unitResult.Unit); + } + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs @@ -85,6 +85,24 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers } /// <summary> + /// Converts ConfigurationTestResult string value to PSConfigurationTestResult. + /// </summary> + /// <param name="value">ConfigurationTestResult value.</param> + /// <returns>PSConfigurationTestResult.</returns> + public static PSConfigurationTestResult ToPSConfigurationTestResult(ConfigurationTestResult value) + { + return value switch + { + ConfigurationTestResult.Unknown => PSConfigurationTestResult.Unknown, + ConfigurationTestResult.Positive => PSConfigurationTestResult.Positive, + ConfigurationTestResult.Negative => PSConfigurationTestResult.Negative, + ConfigurationTestResult.Failed => PSConfigurationTestResult.Failed, + ConfigurationTestResult.NotRun => PSConfigurationTestResult.NotRun, + _ => throw new InvalidOperationException(), + }; + } + + /// <summary> /// Converts ConfigurationUnitState string value to PSConfigurationUnitState. /// </summary> /// <param name="value">ConfigurationUnitState value.</param> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationTestResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationTestResult.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSConfigurationTestResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + /// <summary> + /// Must match ConfigurationTestResult. + /// </summary> + public enum PSConfigurationTestResult + { + /// <summary> + /// The result is unknown. + /// </summary> + Unknown, + + /// <summary> + /// The system is in the state described by the configuration. + /// </summary> + Positive, + + /// <summary> + /// The system is not in the state described by the configuration. + /// </summary> + Negative, + + /// <summary> + /// Running the test failed. + /// </summary> + Failed, + + /// <summary> + /// The test was not run because it was not applicable. + /// </summary> + NotRun, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationSetResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationSetResult.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSTestConfigurationSetResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Helpers; + + /// <summary> + /// Wrapper for TestConfigurationSetResult. + /// </summary> + public class PSTestConfigurationSetResult + { + /// <summary> + /// Initializes a new instance of the <see cref="PSTestConfigurationSetResult"/> class. + /// </summary> + /// <param name="testSetResult">Test set result.</param> + internal PSTestConfigurationSetResult(TestConfigurationSetResult testSetResult) + { + this.TestResult = Utilities.ToPSConfigurationTestResult(testSetResult.TestResult); + + var unitResults = new List<PSTestConfigurationUnitResult>(); + foreach (var unitResult in testSetResult.UnitResults) + { + unitResults.Add(new PSTestConfigurationUnitResult(unitResult)); + } + + this.UnitResults = unitResults; + } + + /// <summary> + /// Gets the test result. + /// </summary> + public PSConfigurationTestResult TestResult { get; private init; } + + /// <summary> + /// Gets the results of the units. + /// </summary> + public IReadOnlyList<PSTestConfigurationUnitResult> UnitResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationUnitResult.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSTestConfigurationUnitResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Helpers; + + /// <summary> + /// Wrapper for TestConfigurationUnitResult. + /// </summary> + public class PSTestConfigurationUnitResult : PSUnitResult + { + /// <summary> + /// Initializes a new instance of the <see cref="PSTestConfigurationUnitResult"/> class. + /// </summary> + /// <param name="testUnitResult">Test unit result.</param> + internal PSTestConfigurationUnitResult(TestConfigurationUnitResult testUnitResult) + : base(testUnitResult.Unit, testUnitResult.ResultInformation) + { + this.TestResult = Utilities.ToPSConfigurationTestResult(testUnitResult.TestResult); + } + + /// <summary> + /// Gets the test result. + /// </summary> + public PSConfigurationTestResult TestResult { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationSetResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationSetResult.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSValidateConfigurationSetResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Exceptions; + + /// <summary> + /// Wrapper for ApplyConfigurationSetResult for validate. + /// </summary> + public class PSValidateConfigurationSetResult + { + /// <summary> + /// Initializes a new instance of the <see cref="PSValidateConfigurationSetResult"/> class. + /// </summary> + /// <param name="applySetResult">Apply set result.</param> + internal PSValidateConfigurationSetResult(ApplyConfigurationSetResult applySetResult) + { + this.ResultCode = applySetResult.ResultCode?.HResult ?? ErrorCodes.S_OK; + + var unitResults = new List<PSValidateConfigurationUnitResult>(); + foreach (var unitResult in applySetResult.UnitResults) + { + unitResults.Add(new PSValidateConfigurationUnitResult(unitResult)); + } + + this.UnitResults = unitResults; + } + + /// <summary> + /// Gets the result code. + /// </summary> + public int ResultCode { get; private init; } + + /// <summary> + /// Gets the results of the units. + /// </summary> + public IReadOnlyList<PSValidateConfigurationUnitResult> UnitResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationUnitResult.cs @@ -0,0 +1,25 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSValidateConfigurationUnitResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using Microsoft.Management.Configuration; + + /// <summary> + /// The validate result of a configuration unit. + /// </summary> + public class PSValidateConfigurationUnitResult : PSUnitResult + { + /// <summary> + /// Initializes a new instance of the <see cref="PSValidateConfigurationUnitResult"/> class. + /// </summary> + /// <param name="unitResult">Apply unit result.</param> + internal PSValidateConfigurationUnitResult(ApplyConfigurationUnitResult unitResult) + : base(unitResult.Unit, unitResult.ResultInformation) + { + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs @@ -504,9 +504,18 @@ namespace Microsoft.WinGet.Configuration.Engine.Resources { /// <summary> /// Looks up a localized string similar to Have you reviewed the configuration and would you like to proceed applying it to the system?. /// </summary> - internal static string ConfigurationWarningPrompt { + internal static string ConfigurationWarningPromptApply { get { - return ResourceManager.GetString("ConfigurationWarningPrompt", resourceCulture); + return ResourceManager.GetString("ConfigurationWarningPromptApply", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Have you reviewed the configuration and would you like to proceed verifying it against the system?. + /// </summary> + internal static string ConfigurationWarningPromptTest { + get { + return ResourceManager.GetString("ConfigurationWarningPromptTest", resourceCulture); } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx @@ -182,7 +182,7 @@ <value>You are responsible for understanding the configuration settings you are choosing to execute. Microsoft is not responsible for the configuration file you have authored or imported. This configuration may change settings in Windows, install software, change software settings (including security settings), and accept user agreements to third-party packages and services on your behalf.  By running this configuration file, you acknowledge that you understand and agree to these resources and settings. Any applications installed are licensed to you by their owners. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages or services.</value> <comment>Legal approved. Do not change without approval.</comment> </data> - <data name="ConfigurationWarningPrompt" xml:space="preserve"> + <data name="ConfigurationWarningPromptApply" xml:space="preserve"> <value>Have you reviewed the configuration and would you like to proceed applying it to the system?</value> <comment>PM approved.</comment> </data> @@ -306,4 +306,7 @@ <value>`-ModulePath` value must be `CurrentUser`, `AllUsers`, `Default` or an absolute path.</value> <comment>{Locked="{-ModulePath}, {CurrentUser}, {AllUsers}, {Default}}</comment> </data> + <data name="ConfigurationWarningPromptTest" xml:space="preserve"> + <value>Have you reviewed the configuration and would you like to proceed verifying it against the system?</value> + </data> </root> \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 b/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 @@ -20,6 +20,9 @@ CmdletsToExport = @( "Get-WinGetConfigurationDetails" "Invoke-WinGetConfiguration" "Start-WinGetConfiguration" + "Test-WinGetConfiguration" + "Confirm-WinGetConfiguration" + "Stop-WinGetConfiguration" ) PrivateData = @{ diff --git a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 @@ -54,7 +54,7 @@ BeforeAll { function DeleteConfigTxtFiles() { - Get-ChildItem $(GetConfigTestDataPath) -Filter Configure*.txt -Recurse | ForEach-Object { Remove-Item $_ } + Get-ChildItem $(GetConfigTestDataPath) -Filter *.txt -Recurse | ForEach-Object { Remove-Item $_ } } function GetConfigTestDataFile([string] $fileName) @@ -294,6 +294,46 @@ Describe 'Get configuration' { $testFile = "c:\dir\fakeFile.txt" { Get-WinGetConfiguration -File $testFile } | Should -Throw $testFile } + + It 'Invalid file' { + $testFile = GetConfigTestDataFile "Empty.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C002*" + } + + It 'Missing property' { + $testFile = GetConfigTestDataFile "NotConfig.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00E*properties*missing*" + } + + It 'Missing configurationVersion' { + $testFile = GetConfigTestDataFile "NoVersion.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00E*configurationVersion*missing*" + } + + It 'Unknown version' { + $testFile = GetConfigTestDataFile "UnknownVersion.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C004*Configuration file version*is not known.*" + } + + It 'Resource wrong type' { + $testFile = GetConfigTestDataFile "ResourcesNotASequence.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C003*resources*wrong type*" + } + + It 'Unit wrong type' { + $testFile = GetConfigTestDataFile "UnitNotAMap.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C003*resources*0*wrong type*" + } + + It 'No resource name' { + $testFile = GetConfigTestDataFile "NoResourceName.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00D*resource*invalid value*Module/*" + } + + It 'Module mismatch' { + $testFile = GetConfigTestDataFile "ModuleMismatch.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00D*invalid value*DifferentModule*" + } } Describe 'Invoke-WinGetConfiguration' { @@ -642,6 +682,161 @@ Describe 'Start|Complete-WinGetConfiguration' { } } +Describe 'Test-WinGetConfiguration' { + + BeforeAll { + DeleteConfigTxtFiles + } + + It 'Negative' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Test-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Negative" + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].TestResult | Should -Be "Negative" + $result.UnitResults[0].ResultCode | Should -Be 0 + } + + It 'Positive' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Set-Content -Path $expectedFile -Value "Contents!" -NoNewline + + $result = Test-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Positive" + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].TestResult | Should -Be "Positive" + $result.UnitResults[0].ResultCode | Should -Be 0 + } + + It 'Piped' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $result = Get-WinGetConfiguration -File $testFile | Test-WinGetConfiguration -AcceptConfigurationAgreements + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Negative" + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].TestResult | Should -Be "Negative" + $result.UnitResults[0].ResultCode | Should -Be 0 + } + + It 'Positional' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Test-WinGetConfiguration -AcceptConfigurationAgreements $set + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Negative" + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].TestResult | Should -Be "Negative" + $result.UnitResults[0].ResultCode | Should -Be 0 + } + + It "Failed" { + $testFile = GetConfigTestDataFile "IndependentResources_OneFailure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Test-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Failed" + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].TestResult | Should -Be "Failed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + $result.UnitResults[1].TestResult | Should -Be "Negative" + $result.UnitResults[1].ResultCode | Should -Be 0 + } +} + +Describe 'Confirm-WinGetConfiguration' { + + It 'Duplicate Identifiers' { + $testFile = GetConfigTestDataFile "DuplicateIdentifiers.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286074 + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be -1978286074 + $result.UnitResults[1].ResultCode | Should -Be -1978286074 + $result.UnitResults[2].ResultCode | Should -Be 0 + } + + It 'Missing dependency' { + $testFile = GetConfigTestDataFile "MissingDependency.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286073 + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be 0 + $result.UnitResults[1].ResultCode | Should -Be 0 + $result.UnitResults[2].ResultCode | Should -Be -1978286073 + } + + It 'Dependency cycle' { + $testFile = GetConfigTestDataFile "DependencyCycle.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286068 + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be -1978286072 + $result.UnitResults[1].ResultCode | Should -Be -1978286072 + $result.UnitResults[2].ResultCode | Should -Be 0 + } + + It 'No issue' { + $testFile = GetConfigTestDataFile "PSGallery_NoSettings.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + } + + It 'Piped' { + $testFile = GetConfigTestDataFile "DuplicateIdentifiers.yml" + $result = Get-WinGetConfiguration -File $testFile | Confirm-WinGetConfiguration + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be -1978286074 + $result.UnitResults[1].ResultCode | Should -Be -1978286074 + $result.UnitResults[2].ResultCode | Should -Be 0 + } + + It 'Positional' { + $testFile = GetConfigTestDataFile "DuplicateIdentifiers.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286074 + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be -1978286074 + $result.UnitResults[1].ResultCode | Should -Be -1978286074 + $result.UnitResults[2].ResultCode | Should -Be 0 + } +} + AfterAll { CleanupGroupPolicies CleanupGroupPolicyKeyIfExists