winget-cli

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

commit 9e3d73cda8310e88bcd334a93b2a564b27701aa9
parent 21e695733e102aaee3cc6e01f1dc5205e5f6d636
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Mon, 11 Sep 2023 14:03:29 -0700

Microsoft.WinGet.Configuration E2E Pester tests (#3605)

Add tests for Microsoft.WinGet.Configuration cmdlets.
Make parameters positional parameters
Return an object with the Apply results for Invoke-WinGetConfiguration and Complete-WinGetConfiguration using Write-Error or returning the same PSConfigurationSet object.
Diffstat:
M.github/actions/spelling/expect.txt | 2++
Msrc/AppInstallerCLIE2ETests/Helpers/TestCommon.cs | 3++-
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs | 2++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs | 1+
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs | 1+
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs | 1+
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs | 1+
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs | 129+++++++++++++++++++++++++++++++++++++++----------------------------------------
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ApplyConfigurationException.cs | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs | 4++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs | 10----------
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/GetDetailsException.cs | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs | 2+-
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs | 161++++++-------------------------------------------------------------------------
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationSetProgressOutputBase.cs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs | 72++++++++++++------------------------------------------------------------
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs | 6+++++-
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs | 21++++++++++++++++++++-
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationSetResult.cs | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationUnitResult.cs | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs | 17++++-------------
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs | 19+++++++++++++------
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationUnitState.cs | 39+++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSGetConfigurationDetailsResult.cs | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSUnitResult.cs | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 | 541+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
26 files changed, 1167 insertions(+), 308 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -124,6 +124,7 @@ EFGH EFile endregion ENDSESSION +EPester epth EQU errmsg @@ -458,6 +459,7 @@ timespan timezone Tlg tombstoned +TOperation TOptions TProgress transitioning diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -916,7 +916,8 @@ namespace AppInstallerCLIE2ETests.Helpers else { string path = customPath; - if (location == TestModuleLocation.WinGetModulePath) + if (location == TestModuleLocation.WinGetModulePath || + location == TestModuleLocation.Default) { path = wingetModulePath; } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs @@ -19,6 +19,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets.Common /// Gets or sets the configuration file. /// </summary> [Parameter( + Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] @@ -28,6 +29,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets.Common /// Gets or sets custom location to install modules. /// </summary> [Parameter( + Position = 1, ValueFromPipelineByPropertyName = true, ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] public string ModulePath { get; set; } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs @@ -23,6 +23,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// Gets or sets the configuration task. /// </summary> [Parameter( + Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs @@ -22,6 +22,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// Gets or sets the configuration set. /// </summary> [Parameter( + Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs @@ -25,6 +25,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// Gets or sets the configuration set. /// </summary> [Parameter( + Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs @@ -24,6 +24,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// Gets or sets the configuration set. /// </summary> [Parameter( + Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { using System; using System.IO; + using System.Linq; using System.Management.Automation; using System.Threading.Tasks; using Microsoft.Management.Configuration; @@ -116,8 +117,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { if (!psConfigurationSet.CanProcess()) { - // TODO: better exception or just write info and return null. - throw new Exception("Someone is using me!!!"); + throw new InvalidOperationException(); } var runningTask = this.RunOnMTA<PSConfigurationSet>( @@ -125,7 +125,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { try { - psConfigurationSet = await this.GetSetDetailsAsync(psConfigurationSet); + psConfigurationSet = await this.GetSetDetailsAsync(psConfigurationSet, false); } finally { @@ -153,16 +153,16 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// <param name="psConfigurationSet">PSConfigurationSet.</param> public void StartApply(PSConfigurationSet psConfigurationSet) { - if (psConfigurationSet.Set.State == ConfigurationSetState.Completed) + // if (psConfigurationSet.Set.State == ConfigurationSetState.Completed) + if (psConfigurationSet.ApplyCompleted) { this.Write(StreamType.Warning, "Processing this set is completed"); - return; + throw new InvalidOperationException(); } if (!psConfigurationSet.CanProcess()) { - // TODO: better exception or just write info and return null. - throw new Exception("Someone is using me!!!"); + throw new InvalidOperationException(); } var configurationJob = this.StartApplyInternal(psConfigurationSet); @@ -181,24 +181,24 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// <param name="psConfigurationJob">The configuration job.</param> public void Continue(PSConfigurationJob psConfigurationJob) { - if (psConfigurationJob.ConfigurationTask.IsCompleted) + if (psConfigurationJob.ApplyConfigurationTask.IsCompleted) { // It is safe to print all output. psConfigurationJob.StartCommand.ConsumeAndWriteStreams(this); this.Write(StreamType.Verbose, "The task was completed before waiting"); - if (psConfigurationJob.ConfigurationTask.IsCompletedSuccessfully) + if (psConfigurationJob.ApplyConfigurationTask.IsCompletedSuccessfully) { this.Write(StreamType.Verbose, "Completed successfully"); - this.Write(StreamType.Object, psConfigurationJob.ConfigurationTask.Result); + this.Write(StreamType.Object, psConfigurationJob.ApplyConfigurationTask.Result); return; } - else if (psConfigurationJob.ConfigurationTask.IsFaulted) + else if (psConfigurationJob.ApplyConfigurationTask.IsFaulted) { this.Write(StreamType.Verbose, "Completed faulted before waiting"); // Maybe just write error? - throw psConfigurationJob.ConfigurationTask.Exception!; + throw psConfigurationJob.ApplyConfigurationTask.Exception!; } } @@ -209,8 +209,8 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { // Signal the command that it can write to streams and wait for task. this.Write(StreamType.Verbose, "Waiting for task to complete"); - psConfigurationJob.StartCommand.Wait(psConfigurationJob.ConfigurationTask, this); - this.Write(StreamType.Object, psConfigurationJob.ConfigurationTask.Result); + psConfigurationJob.StartCommand.Wait(psConfigurationJob.ApplyConfigurationTask, this); + this.Write(StreamType.Object, psConfigurationJob.ApplyConfigurationTask.Result); } private IConfigurationSetProcessorFactory CreateFactory(OpenConfigurationParameters openParams) @@ -259,31 +259,29 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { psConfigurationSet.PsProcessor.UpdateDiagnosticCmdlet(this); - var runningTask = this.RunOnMTA<PSConfigurationSet>( + var runningTask = this.RunOnMTA<PSApplyConfigurationSetResult>( async () => { try { - psConfigurationSet = await this.ApplyConfigurationAsync(psConfigurationSet); + return await this.ApplyConfigurationAsync(psConfigurationSet); } finally { this.Complete(); psConfigurationSet.DoneProcessing(); } - - return psConfigurationSet; }); return new PSConfigurationJob(runningTask, this); } - private async Task<PSConfigurationSet> ApplyConfigurationAsync(PSConfigurationSet psConfigurationSet) + private async Task<PSApplyConfigurationSetResult> ApplyConfigurationAsync(PSConfigurationSet psConfigurationSet) { if (!psConfigurationSet.HasDetails) { this.Write(StreamType.Verbose, "Getting details for configuration set"); - await this.GetSetDetailsAsync(psConfigurationSet); + await this.GetSetDetailsAsync(psConfigurationSet, true); } var processor = psConfigurationSet.PsProcessor.Processor; @@ -303,17 +301,18 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands try { var result = await applyTask; - applyProgressOutput.HandleUnreportedProgress(result); + applyProgressOutput.HandleProgress(result); + + return new PSApplyConfigurationSetResult(result); } finally { applyProgressOutput.CompleteProgress(); + psConfigurationSet.ApplyCompleted = true; } - - return psConfigurationSet; } - private async Task<PSConfigurationSet> GetSetDetailsAsync(PSConfigurationSet psConfigurationSet) + private async Task<PSConfigurationSet> GetSetDetailsAsync(PSConfigurationSet psConfigurationSet, bool warnOnError) { var processor = psConfigurationSet.PsProcessor.Processor; var set = psConfigurationSet.Set; @@ -325,56 +324,54 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands return psConfigurationSet; } - var detailsProgressOutput = new GetConfigurationSetDetailsProgressOutput( - this, - this.GetNewProgressActivityId(), - Resources.ConfigurationGettingDetails, - Resources.OperationInProgress, - Resources.OperationCompleted, - totalUnitsCount); - - var detailsTask = processor.GetSetDetailsAsync(set, ConfigurationUnitDetailFlags.ReadOnly); - detailsTask.Progress = detailsProgressOutput.Progress; - try { - var result = await detailsTask; - detailsProgressOutput.HandleUnits(result.UnitResults); - } - catch (Exception e) - { - this.WriteError( - ErrorRecordErrorId.ConfigurationDetailsError, - e); - } - finally - { - detailsProgressOutput.CompleteProgress(); - } + var detailsProgressOutput = new GetConfigurationSetDetailsProgressOutput( + this, + this.GetNewProgressActivityId(), + Resources.ConfigurationGettingDetails, + Resources.OperationInProgress, + Resources.OperationCompleted, + totalUnitsCount); + + var detailsTask = processor.GetSetDetailsAsync(set, ConfigurationUnitDetailFlags.ReadOnly); + detailsTask.Progress = detailsProgressOutput.Progress; + + try + { + var result = await detailsTask; + detailsProgressOutput.HandleProgress(result); - if (detailsProgressOutput.UnitsShown == 0) - { - this.Write(StreamType.Warning, Resources.ConfigurationFailedToGetDetails); + if (result.UnitResults.Where(u => u.ResultInformation.ResultCode != null).Any()) + { + throw new GetDetailsException(result.UnitResults); + } + + if (detailsProgressOutput.UnitsShown == 0) + { + throw new GetDetailsException(); + } + + psConfigurationSet.HasDetails = true; + } + finally + { + detailsProgressOutput.CompleteProgress(); + } } - else + catch (Exception e) { - psConfigurationSet.HasDetails = true; + if (warnOnError) + { + this.Write(StreamType.Warning, e.Message); + } + else + { + throw; + } } return psConfigurationSet; } - - private void LogFailedGetConfigurationUnitDetails(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInformation) - { - if (resultInformation.ResultCode != null) - { - string errorMessage = $"Failed to get unit details for {unit.Type} 0x{resultInformation.ResultCode.HResult:X}" + - $"{Environment.NewLine}Description: '{resultInformation.Description}'{Environment.NewLine}Details: '{resultInformation.Details}'"; - this.WriteError( - ErrorRecordErrorId.ConfigurationDetailsError, - errorMessage, - resultInformation.ResultCode); - } - } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ApplyConfigurationException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ApplyConfigurationException.cs @@ -0,0 +1,43 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ApplyConfigurationException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Exceptions +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + using Microsoft.WinGet.Configuration.Engine.Resources; + + /// <summary> + /// Exception thrown when there's an error when configuration is applied. + /// </summary> + public class ApplyConfigurationException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="ApplyConfigurationException"/> class. + /// </summary> + /// <param name="applyResult">Apply Result.</param> + internal ApplyConfigurationException(ApplyConfigurationSetResult applyResult) + : base(Resources.ConfigurationFailedToApply) + { + this.HResult = applyResult.ResultCode?.HResult ?? ErrorCodes.WingetConfigErrorSetApplyFailed; + + var results = new List<PSApplyConfigurationUnitResult>(); + foreach (var unitResult in applyResult.UnitResults) + { + results.Add(new PSApplyConfigurationUnitResult(unitResult)); + } + + this.UnitResults = results; + } + + /// <summary> + /// Gets the result of the units. + /// </summary> + public IReadOnlyList<PSApplyConfigurationUnitResult> UnitResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs @@ -12,6 +12,9 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions internal static class ErrorCodes { #pragma warning disable SA1600 // ElementsMustBeDocumented +#pragma warning disable SA1310 // Field names should not contain underscore + internal const int S_OK = 0; + internal const int WingetConfigErrorInvalidConfigurationFile = unchecked((int)0x8A15C001); internal const int WingetConfigErrorInvalidYaml = unchecked((int)0x8A15C002); internal const int WingetConfigErrorInvalidFieldType = unchecked((int)0x8A15C003); @@ -36,6 +39,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions internal const int WinGetConfigUnitModuleConflict = unchecked((int)0x8A15C107); internal const int WinGetConfigUnitImportModule = unchecked((int)0x8A15C108); internal const int WinGetConfigUnitInvokeInvalidResult = unchecked((int)0x8A15C109); +#pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1600 // ElementsMustBeDocumented } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs @@ -15,15 +15,5 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions /// Error message from diagnostics. /// </summary> ConfigurationDiagnosticError, - - /// <summary> - /// Error processing details. - /// </summary> - ConfigurationDetailsError, - - /// <summary> - /// Error applying configuration. - /// </summary> - ConfigurationApplyError, } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/GetDetailsException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/GetDetailsException.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetDetailsException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Exceptions +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + using Microsoft.WinGet.Configuration.Engine.Resources; + + /// <summary> + /// Exception thrown while getting details. + /// </summary> + public class GetDetailsException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="GetDetailsException"/> class. + /// </summary> + /// <param name="unitResults">Unit results.</param> + internal GetDetailsException(IReadOnlyList<GetConfigurationUnitDetailsResult>? unitResults = null) + : base(Resources.ConfigurationFailedToGetDetails) + { + var results = new List<PSGetConfigurationDetailsResult>(); + + if (unitResults != null) + { + foreach (var result in unitResults) + { + results.Add(new PSGetConfigurationDetailsResult(result)); + } + } + + this.UnitDetailsResults = results; + } + + /// <summary> + /// Gets the unit details result. + /// </summary> + public IReadOnlyList<PSGetConfigurationDetailsResult> UnitDetailsResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs @@ -21,7 +21,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions /// </summary> /// <param name="openResult">Open Result.</param> /// <param name="configurationFile">Configuration file.</param> - public OpenConfigurationSetException(OpenConfigurationSetResult openResult, string configurationFile) + internal OpenConfigurationSetException(OpenConfigurationSetResult openResult, string configurationFile) : base(GetMessage(openResult, configurationFile)) { } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs @@ -6,30 +6,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { - using System; - using System.Collections.Generic; - using System.Text; using Microsoft.Management.Configuration; using Microsoft.WinGet.Configuration.Engine.Commands; - using Microsoft.WinGet.Configuration.Engine.Exceptions; - using Microsoft.WinGet.Configuration.Engine.Resources; using Windows.Foundation; - using static Microsoft.WinGet.Configuration.Engine.Commands.AsyncCommand; /// <summary> - /// Helper to handle progress callbacks from ApplyConfigurationSetAsync. + /// Helper to handle progress callbacks from ApplySetAsync. /// </summary> - internal class ApplyConfigurationSetProgressOutput + internal class ApplyConfigurationSetProgressOutput : ConfigurationSetProgressOutputBase<ApplyConfigurationSetResult, ConfigurationSetChangeData> { - private readonly AsyncCommand cmd; - private readonly int activityId; - private readonly string activity; - private readonly string inProgressMessage; - private readonly string completeMessage; - private readonly int totalUnitsExpected; - - private readonly HashSet<Guid> unitsCompleted = new (); - private bool isFirstProgress = true; /// <summary> @@ -42,66 +27,42 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <param name="completeMessage">The activity complete message.</param> /// <param name="totalUnitsExpected">Total of units expected.</param> public ApplyConfigurationSetProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) { - this.cmd = cmd; - this.activityId = activityId; - this.activity = activity; - this.inProgressMessage = inProgressMessage; - this.completeMessage = completeMessage; - this.totalUnitsExpected = totalUnitsExpected; - - // Write initial progress record. - // For some reason, if this is 0 the progress bar is shown full. Start with 1% - this.cmd.WriteProgressWithPercentage(activityId, activity, $"{this.inProgressMessage} 0/{this.totalUnitsExpected}", 1, 100); } - /// <summary> - /// Progress callback. - /// </summary> - /// <param name="operation">Async operation in progress.</param> - /// <param name="data">Change data.</param> - public void Progress(IAsyncOperationWithProgress<ApplyConfigurationSetResult, ConfigurationSetChangeData> operation, ConfigurationSetChangeData data) + /// <inheritdoc/> + public override void Progress(IAsyncOperationWithProgress<ApplyConfigurationSetResult, ConfigurationSetChangeData> operation, ConfigurationSetChangeData data) { if (this.isFirstProgress) { - this.HandleUnreportedProgress(operation.GetResults()); + this.HandleProgress(operation.GetResults()); } switch (data.Change) { case ConfigurationSetChangeEventType.UnitStateChanged: - this.HandleUnitProgress(data.Unit, data.UnitState, data.ResultInformation); + this.HandleUnitProgress(data.Unit, data.UnitState); break; } } - /// <summary> - /// Handle unreported progress. - /// </summary> - /// <param name="result">Set result.</param> - public void HandleUnreportedProgress(ApplyConfigurationSetResult result) + /// <inheritdoc/> + public override void HandleProgress(ApplyConfigurationSetResult result) { if (!this.isFirstProgress) { this.isFirstProgress = false; foreach (var unitResult in result.UnitResults) { - this.HandleUnitProgress(unitResult.Unit, unitResult.State, unitResult.ResultInformation); + this.HandleUnitProgress(unitResult.Unit, unitResult.State); } } } - /// <summary> - /// Completes the progress bar. - /// </summary> - public void CompleteProgress() + private void HandleUnitProgress(ConfigurationUnit unit, ConfigurationUnitState state) { - this.cmd.CompleteProgress(this.activityId, this.activity, this.completeMessage); - } - - private void HandleUnitProgress(ConfigurationUnit unit, ConfigurationUnitState state, IConfigurationUnitResultInformation resultInformation) - { - if (this.unitsCompleted.Contains(unit.InstanceIdentifier)) + if (this.UnitsCompleted.Contains(unit.InstanceIdentifier)) { return; } @@ -114,108 +75,10 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers case ConfigurationUnitState.InProgress: break; case ConfigurationUnitState.Completed: - if (resultInformation.ResultCode != null) - { - string description = resultInformation.Description.Trim(); - var message = this.GetUnitFailedMessage(unit, resultInformation); - - string errorMessage = $"Configuration unit {unit.Type}[{unit.Identifier}] failed with code 0x{resultInformation.ResultCode.HResult:X}" + - $" and error message:\n{description}\n{resultInformation.Details}\n{message}"; - this.cmd.WriteError( - ErrorRecordErrorId.ConfigurationApplyError, - errorMessage, - resultInformation.ResultCode); - } - - this.CompleteUnit(unit); - break; case ConfigurationUnitState.Skipped: - this.cmd.Write(StreamType.Warning, this.GetUnitSkippedMessage(resultInformation)); this.CompleteUnit(unit); break; } } - - private void CompleteUnit(ConfigurationUnit unit) - { - if (this.unitsCompleted.Add(unit.InstanceIdentifier)) - { - this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.unitsCompleted.Count}/{this.totalUnitsExpected}", this.unitsCompleted.Count, this.totalUnitsExpected); - } - } - - private string GetUnitFailedMessage(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInformation) - { - if (resultInformation.ResultCode == null) - { - return string.Format(Resources.ConfigurationUnitFailed, "null"); - } - - int resultCode = resultInformation.ResultCode.HResult; - switch (resultCode) - { - case ErrorCodes.WingetConfigErrorDuplicateIdentifier: - return string.Format(Resources.ConfigurationUnitHasDuplicateIdentifier, unit.Identifier); - case ErrorCodes.WingetConfigErrorMissingDependency: - return string.Format(Resources.ConfigurationUnitHasMissingDependency, resultInformation.Details); - case ErrorCodes.WingetConfigErrorAssertionFailed: - return Resources.ConfigurationUnitAssertHadNegativeResult; - case ErrorCodes.WinGetConfigUnitNotFound: - return Resources.ConfigurationUnitNotFoundInModule; - case ErrorCodes.WinGetConfigUnitNotFoundRepository: - return Resources.ConfigurationUnitNotFound; - case ErrorCodes.WinGetConfigUnitMultipleMatches: - return Resources.ConfigurationUnitMultipleMatches; - case ErrorCodes.WinGetConfigUnitInvokeGet: - return Resources.ConfigurationUnitFailedDuringGet; - case ErrorCodes.WinGetConfigUnitInvokeTest: - return Resources.ConfigurationUnitFailedDuringTest; - case ErrorCodes.WinGetConfigUnitInvokeSet: - return Resources.ConfigurationUnitFailedDuringSet; - case ErrorCodes.WinGetConfigUnitModuleConflict: - return Resources.ConfigurationUnitModuleConflict; - case ErrorCodes.WinGetConfigUnitImportModule: - return Resources.ConfigurationUnitModuleImportFailed; - case ErrorCodes.WinGetConfigUnitInvokeInvalidResult: - return Resources.ConfigurationUnitReturnedInvalidResult; - } - - switch (resultInformation.ResultSource) - { - case ConfigurationUnitResultSource.ConfigurationSet: - return string.Format(Resources.ConfigurationUnitFailedConfigSet, resultCode); - case ConfigurationUnitResultSource.Internal: - return string.Format(Resources.ConfigurationUnitFailedInternal, resultCode); - case ConfigurationUnitResultSource.Precondition: - return string.Format(Resources.ConfigurationUnitFailedPrecondition, resultCode); - case ConfigurationUnitResultSource.SystemState: - return string.Format(Resources.ConfigurationUnitFailedSystemState, resultCode); - case ConfigurationUnitResultSource.UnitProcessing: - return string.Format(Resources.ConfigurationUnitFailedUnitProcessing, resultCode); - } - - return string.Format(Resources.ConfigurationUnitFailed, resultCode); - } - - private string GetUnitSkippedMessage(IConfigurationUnitResultInformation resultInformation) - { - if (resultInformation.ResultCode == null) - { - return string.Format(Resources.ConfigurationUnitSkipped, "null"); - } - - int resultCode = resultInformation.ResultCode.HResult; - switch (resultCode) - { - case ErrorCodes.WingetConfigErrorManuallySkipped: - return Resources.ConfigurationUnitManuallySkipped; - case ErrorCodes.WingetConfigErrorDependencyUnsatisfied: - return Resources.ConfigurationUnitNotRunDueToDependency; - case ErrorCodes.WingetConfigErrorAssertionFailed: - return Resources.ConfigurationUnitNotRunDueToFailedAssert; - } - - return string.Format(Resources.ConfigurationUnitSkipped, resultCode); - } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationSetProgressOutputBase.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationSetProgressOutputBase.cs @@ -0,0 +1,90 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationSetProgressOutputBase.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Helpers +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Windows.Foundation; + + /// <summary> + /// Helper to handle progress callbacks. + /// </summary> + /// <typeparam name="TOperationResult">The operation result.</typeparam> + /// <typeparam name="TProgressData">Progress data.</typeparam> + internal abstract class ConfigurationSetProgressOutputBase<TOperationResult, TProgressData> + { + private readonly AsyncCommand cmd; + private readonly int activityId; + private readonly string activity; + private readonly string inProgressMessage; + private readonly string completeMessage; + private readonly int totalUnitsExpected; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationSetProgressOutputBase{TOperationResult, TProgressData}"/> 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 ConfigurationSetProgressOutputBase(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + { + this.cmd = cmd; + this.activityId = activityId; + this.activity = activity; + this.inProgressMessage = inProgressMessage; + this.completeMessage = completeMessage; + this.totalUnitsExpected = totalUnitsExpected; + + // Write initial progress record. + // For some reason, if this is 0 the progress bar is shown full. Start with 1% + this.cmd.WriteProgressWithPercentage(activityId, activity, $"{this.inProgressMessage} 0/{this.totalUnitsExpected}", 1, 100); + } + + /// <summary> + /// Gets or sets a hash set with the completed units. + /// </summary> + protected HashSet<Guid> UnitsCompleted { get; set; } = new (); + + /// <summary> + /// Progress callback. + /// </summary> + /// <param name="operation">Async operation in progress.</param> + /// <param name="data">Change data.</param> + public abstract void Progress(IAsyncOperationWithProgress<TOperationResult, TProgressData> operation, TProgressData data); + + /// <summary> + /// Handle progress. + /// </summary> + /// <param name="result">Set result.</param> + public abstract void HandleProgress(TOperationResult result); + + /// <summary> + /// Completes the progress bar. + /// </summary> + public void CompleteProgress() + { + this.cmd.CompleteProgress(this.activityId, this.activity, this.completeMessage); + } + + /// <summary> + /// Marks a unit as completed and increase progress. + /// </summary> + /// <param name="unit">Unit.</param> + protected void CompleteUnit(ConfigurationUnit unit) + { + if (this.UnitsCompleted.Add(unit.InstanceIdentifier)) + { + this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.UnitsCompleted.Count}/{this.totalUnitsExpected}", this.UnitsCompleted.Count, this.totalUnitsExpected); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs @@ -6,26 +6,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { - using System; - using System.Collections.Generic; using Microsoft.Management.Configuration; using Microsoft.WinGet.Configuration.Engine.Commands; - using Microsoft.WinGet.Configuration.Engine.Exceptions; using Windows.Foundation; - using static Microsoft.WinGet.Configuration.Engine.Commands.AsyncCommand; /// <summary> /// Helper to handle progress callback from GetSetDetailsAsync. /// </summary> - internal class GetConfigurationSetDetailsProgressOutput + internal class GetConfigurationSetDetailsProgressOutput : ConfigurationSetProgressOutputBase<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult> { - private readonly AsyncCommand cmd; - private readonly int activityId; - private readonly string activity; - private readonly string inProgressMessage; - private readonly string completeMessage; - private readonly int totalUnitsExpected; - /// <summary> /// Initializes a new instance of the <see cref="GetConfigurationSetDetailsProgressOutput"/> class. /// </summary> @@ -36,67 +25,30 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <param name="completeMessage">The activity complete message.</param> /// <param name="totalUnitsExpected">Total of units expected.</param> public GetConfigurationSetDetailsProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) { - this.cmd = cmd; - this.activityId = activityId; - this.activity = activity; - this.inProgressMessage = inProgressMessage; - this.completeMessage = completeMessage; - this.totalUnitsExpected = totalUnitsExpected; - - // Write initial progress record. - // For some reason, if this is 0 the progress bar is shown full. Start with 1% - this.cmd.WriteProgressWithPercentage(activityId, activity, $"{this.inProgressMessage} 0/{this.totalUnitsExpected}", 1, 100); - } - - /// <summary> - /// Gets the number of units shown. - /// </summary> - internal int UnitsShown { get; private set; } = 0; - - /// <summary> - /// Progress callback. - /// </summary> - /// <param name="operation">Async operation in progress.</param> - /// <param name="result">Result.</param> - public void Progress(IAsyncOperationWithProgress<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult> operation, GetConfigurationUnitDetailsResult result) - { - this.HandleUnits(operation.GetResults().UnitResults); } /// <summary> - /// Handle units. + /// Gets the units shown. /// </summary> - /// <param name="unitResults">The unit results.</param> - public void HandleUnits(IReadOnlyList<GetConfigurationUnitDetailsResult> unitResults) + public int UnitsShown { - while (this.UnitsShown < unitResults.Count) - { - GetConfigurationUnitDetailsResult unitResult = unitResults[this.UnitsShown]; - this.LogFailedGetConfigurationUnitDetails(unitResult.Unit, unitResult.ResultInformation); - ++this.UnitsShown; - this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.UnitsShown}/{this.totalUnitsExpected}", this.UnitsShown, this.totalUnitsExpected); - } + get { return this.UnitsCompleted.Count; } } - /// <summary> - /// Complete progress. - /// </summary> - public void CompleteProgress() + /// <inheritdoc/> + public override void Progress(IAsyncOperationWithProgress<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult> operation, GetConfigurationUnitDetailsResult result) { - this.cmd.CompleteProgress(this.activityId, this.activity, this.completeMessage); + this.HandleProgress(operation.GetResults()); } - private void LogFailedGetConfigurationUnitDetails(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInformation) + /// <inheritdoc/> + public override void HandleProgress(GetConfigurationSetDetailsResult result) { - if (resultInformation.ResultCode != null) + foreach (var unitResult in result.UnitResults) { - string errorMessage = $"Failed to get unit details for {unit.Type} 0x{resultInformation.ResultCode.HResult:X}" + - $"{Environment.NewLine}Description: '{resultInformation.Description}'{Environment.NewLine}Details: '{resultInformation.Details}'"; - this.cmd.WriteError( - ErrorRecordErrorId.ConfigurationDetailsError, - errorMessage, - resultInformation.ResultCode); + this.CompleteUnit(unitResult.Unit); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs @@ -77,6 +77,10 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers psCmdlet.SessionState.Path.CurrentFileSystemLocation.Path, filePath)); } + else + { + filePath = Path.GetFullPath(filePath); + } if (!File.Exists(filePath)) { @@ -129,7 +133,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers throw new ArgumentException(Resources.ConfigurationModulePathArgError); } - this.CustomLocation = customLocation; + this.CustomLocation = Path.GetFullPath(customLocation); this.Location = PowerShellConfigurationProcessorLocation.Custom; } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs @@ -7,11 +7,12 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { using System; - using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; using System.Security.Principal; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.PSObjects; /// <summary> /// Helper methods. @@ -82,5 +83,23 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers return lines; } + + /// <summary> + /// Converts ConfigurationUnitState string value to PSConfigurationUnitState. + /// </summary> + /// <param name="value">ConfigurationUnitState value.</param> + /// <returns>PSConfigurationUnitState.</returns> + public static PSConfigurationUnitState ToPSConfigurationUnitState(ConfigurationUnitState value) + { + return value switch + { + ConfigurationUnitState.Unknown => PSConfigurationUnitState.Unknown, + ConfigurationUnitState.Pending => PSConfigurationUnitState.Pending, + ConfigurationUnitState.InProgress => PSConfigurationUnitState.InProgress, + ConfigurationUnitState.Completed => PSConfigurationUnitState.Completed, + ConfigurationUnitState.Skipped => PSConfigurationUnitState.Skipped, + _ => throw new InvalidOperationException(), + }; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationSetResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationSetResult.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSApplyConfigurationSetResult.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. + /// </summary> + public class PSApplyConfigurationSetResult + { + /// <summary> + /// Initializes a new instance of the <see cref="PSApplyConfigurationSetResult"/> class. + /// </summary> + /// <param name="applySetResult">Apply set result.</param> + internal PSApplyConfigurationSetResult(ApplyConfigurationSetResult applySetResult) + { + this.ResultCode = applySetResult.ResultCode?.HResult ?? ErrorCodes.S_OK; + + var unitResults = new List<PSApplyConfigurationUnitResult>(); + foreach (var unitResult in applySetResult.UnitResults) + { + unitResults.Add(new PSApplyConfigurationUnitResult(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<PSApplyConfigurationUnitResult> UnitResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationUnitResult.cs @@ -0,0 +1,43 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSApplyConfigurationUnitResult.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> + /// The apply result of a configuration unit. + /// </summary> + public class PSApplyConfigurationUnitResult : PSUnitResult + { + /// <summary> + /// Initializes a new instance of the <see cref="PSApplyConfigurationUnitResult"/> class. + /// </summary> + /// <param name="unitResult">Apply unit result.</param> + internal PSApplyConfigurationUnitResult(ApplyConfigurationUnitResult unitResult) + : base(unitResult.Unit, unitResult.ResultInformation) + { + this.State = Utilities.ToPSConfigurationUnitState(unitResult.State); + this.PreviouslyInDesiredState = unitResult.PreviouslyInDesiredState; + } + + /// <summary> + /// Gets the unit state. + /// </summary> + public PSConfigurationUnitState State { get; private init; } + + /// <summary> + /// Gets a value indicating whether the unit was in a previous desired state. + /// </summary> + public bool PreviouslyInDesiredState { get; private init; } + + /// <summary> + /// Gets a value indicating whether a reboot is required after the configuration unit was applied. + /// </summary> + public bool RebootRequired { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs @@ -18,33 +18,24 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects /// <summary> /// Initializes a new instance of the <see cref="PSConfigurationJob"/> class. /// </summary> - /// <param name="configTask">The configuration task.</param> + /// <param name="applyConfigTask">The apply configuration task.</param> /// <param name="startCommand">The start command.</param> internal PSConfigurationJob( - Task<PSConfigurationSet> configTask, + Task<PSApplyConfigurationSetResult> applyConfigTask, AsyncCommand startCommand) { - this.ConfigurationTask = configTask; + this.ApplyConfigurationTask = applyConfigTask; this.StartCommand = startCommand; } /// <summary> /// Gets the running configuration task. /// </summary> - internal Task<PSConfigurationSet> ConfigurationTask { get; private set; } + internal Task<PSApplyConfigurationSetResult> ApplyConfigurationTask { get; private set; } /// <summary> /// Gets the command that started async operation. /// </summary> internal AsyncCommand StartCommand { get; private set; } - - /// <summary> - /// Gets the status of the configuration task. - /// </summary> - /// <returns>The task status.</returns> - public string GetStatus() - { - return this.ConfigurationTask.Status.ToString(); - } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs @@ -62,28 +62,35 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects } /// <summary> - /// Gets the state. + /// Gets the schema version. /// </summary> - public string State + public string SchemaVersion { get { - return this.Set.State.ToString(); + return this.Set.SchemaVersion; } } /// <summary> - /// Gets the schema version. + /// Gets the state. + /// TODO: enable once implemented. /// </summary> - public string SchemaVersion + internal string State { get { - return this.Set.SchemaVersion; + return this.Set.State.ToString(); } } /// <summary> + /// Gets or sets a value indicating whether apply ran. + /// TODO: remove once State is implemented. + /// </summary> + internal bool ApplyCompleted { get; set; } + + /// <summary> /// Gets the PSConfigurationProcessor. /// </summary> internal PSConfigurationProcessor PsProcessor { get; private set; } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationUnitState.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationUnitState.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSConfigurationUnitState.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + /// <summary> + /// Must match ConfigurationUnitState. + /// </summary> + public enum PSConfigurationUnitState + { + /// <summary> + /// The state of the configuration unit is unknown. + /// </summary> + Unknown, + + /// <summary> + /// The configuration unit is in the queue to be applied. + /// </summary> + Pending, + + /// <summary> + /// The configuration unit is actively being applied. + /// </summary> + InProgress, + + /// <summary> + /// The configuration unit has completed being applied. + /// </summary> + Completed, + + /// <summary> + /// The configuration unit was not applied due to external factors. + /// </summary> + Skipped, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSGetConfigurationDetailsResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSGetConfigurationDetailsResult.cs @@ -0,0 +1,49 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSGetConfigurationDetailsResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using System; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Exceptions; + + /// <summary> + /// Result for getting the details of a unit. + /// </summary> + public class PSGetConfigurationDetailsResult + { + /// <summary> + /// Initializes a new instance of the <see cref="PSGetConfigurationDetailsResult"/> class. + /// </summary> + /// <param name="result">Get unit details result.</param> + internal PSGetConfigurationDetailsResult(GetConfigurationUnitDetailsResult result) + { + this.Type = result.Unit.Type; + this.ResultCode = result.ResultInformation?.ResultCode?.HResult ?? ErrorCodes.S_OK; + + if (result.ResultInformation?.ResultCode != null) + { + this.ErrorMessage = $"Failed to get unit details for {this.Type} 0x{this.ResultCode:X}" + + $"{Environment.NewLine}Description: '{result.ResultInformation.Description}'{Environment.NewLine}Details: '{result.ResultInformation.Details}'"; + } + } + + /// <summary> + /// Gets the unit type. + /// </summary> + public string Type { get; private init; } + + /// <summary> + /// Gets the result code. + /// </summary> + public int ResultCode { get; private init; } + + /// <summary> + /// Gets the error message. + /// </summary> + public string? ErrorMessage { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSUnitResult.cs @@ -0,0 +1,128 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PSUnitResult.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.Exceptions; + using Microsoft.WinGet.Configuration.Engine.Resources; + + /// <summary> + /// Unit result. + /// </summary> + public abstract class PSUnitResult + { + /// <summary> + /// Initializes a new instance of the <see cref="PSUnitResult"/> class. + /// </summary> + /// <param name="unit">Unit.</param> + /// <param name="resultInfo">Result info.</param> + internal PSUnitResult(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInfo) + { + this.Type = unit.Type; + this.ResultCode = resultInfo.ResultCode?.HResult ?? ErrorCodes.S_OK; + + if (this.ResultCode != ErrorCodes.S_OK) + { + this.Message = this.GetUnitMessage(unit, resultInfo); + this.Description = resultInfo.Description.Trim(); + this.Details = resultInfo.Details; + } + } + + /// <summary> + /// Gets the unit type. + /// </summary> + public string Type { get; private init; } + + /// <summary> + /// Gets the result code. + /// </summary> + public int ResultCode { get; private init; } + + /// <summary> + /// Gets the message. + /// </summary> + public string? Message { get; private init; } + + /// <summary> + /// Gets the short description. + /// </summary> + public string? Description { get; private init; } + + /// <summary> + /// Gets detailed information. + /// </summary> + public string? Details { get; private init; } + + private string GetUnitMessage(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInfo) + { + if (resultInfo.ResultCode == null) + { + if (unit.State == ConfigurationUnitState.Skipped) + { + return string.Format(Resources.ConfigurationUnitSkipped, "null"); + } + + return string.Format(Resources.ConfigurationUnitFailed, "null"); + } + + int resultCode = resultInfo.ResultCode.HResult; + switch (resultCode) + { + case ErrorCodes.WingetConfigErrorDuplicateIdentifier: + return string.Format(Resources.ConfigurationUnitHasDuplicateIdentifier, unit.Identifier); + case ErrorCodes.WingetConfigErrorMissingDependency: + return string.Format(Resources.ConfigurationUnitHasMissingDependency, resultInfo.Details); + case ErrorCodes.WingetConfigErrorAssertionFailed: + return Resources.ConfigurationUnitAssertHadNegativeResult; + case ErrorCodes.WinGetConfigUnitNotFound: + return Resources.ConfigurationUnitNotFoundInModule; + case ErrorCodes.WinGetConfigUnitNotFoundRepository: + return Resources.ConfigurationUnitNotFound; + case ErrorCodes.WinGetConfigUnitMultipleMatches: + return Resources.ConfigurationUnitMultipleMatches; + case ErrorCodes.WinGetConfigUnitInvokeGet: + return Resources.ConfigurationUnitFailedDuringGet; + case ErrorCodes.WinGetConfigUnitInvokeTest: + return Resources.ConfigurationUnitFailedDuringTest; + case ErrorCodes.WinGetConfigUnitInvokeSet: + return Resources.ConfigurationUnitFailedDuringSet; + case ErrorCodes.WinGetConfigUnitModuleConflict: + return Resources.ConfigurationUnitModuleConflict; + case ErrorCodes.WinGetConfigUnitImportModule: + return Resources.ConfigurationUnitModuleImportFailed; + case ErrorCodes.WinGetConfigUnitInvokeInvalidResult: + return Resources.ConfigurationUnitReturnedInvalidResult; + case ErrorCodes.WingetConfigErrorManuallySkipped: + return Resources.ConfigurationUnitManuallySkipped; + case ErrorCodes.WingetConfigErrorDependencyUnsatisfied: + return Resources.ConfigurationUnitNotRunDueToDependency; + } + + switch (resultInfo.ResultSource) + { + case ConfigurationUnitResultSource.ConfigurationSet: + return string.Format(Resources.ConfigurationUnitFailedConfigSet, resultCode); + case ConfigurationUnitResultSource.Internal: + return string.Format(Resources.ConfigurationUnitFailedInternal, resultCode); + case ConfigurationUnitResultSource.Precondition: + return string.Format(Resources.ConfigurationUnitFailedPrecondition, resultCode); + case ConfigurationUnitResultSource.SystemState: + return string.Format(Resources.ConfigurationUnitFailedSystemState, resultCode); + case ConfigurationUnitResultSource.UnitProcessing: + return string.Format(Resources.ConfigurationUnitFailedUnitProcessing, resultCode); + } + + if (unit.State == ConfigurationUnitState.Skipped) + { + return string.Format(Resources.ConfigurationUnitSkipped, resultCode); + } + + return string.Format(Resources.ConfigurationUnitFailed, resultCode); + } + } +} diff --git a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 @@ -5,12 +5,15 @@ .Synopsis Pester tests related to the Microsoft.WinGet.Configuration PowerShell module. 'Invoke-Pester' should be called in an admin PowerShell window. + Requires local test repo to be setup. #> BeforeAll { + $env:POWERSHELL_TELEMETRY_OPTOUT = "true" $deviceGroupPolicyRoot = "HKLM:\Software\Policies\Microsoft\Windows" $wingetPolicyKeyName = "AppInstaller" $wingetGroupPolicyRegistryRoot = $deviceGroupPolicyRoot + "\" + $wingetPolicyKeyName + $e2eTestModule = "xE2ETestResource" Import-Module Microsoft.WinGet.Configuration @@ -43,6 +46,160 @@ BeforeAll { Remove-ItemProperty -Path $wingetGroupPolicyRegistryRoot -Name * } } + + function GetConfigTestDataPath() + { + return Join-Path $PSScriptRoot "..\..\AppInstallerCLIE2ETests\TestData\Configuration\" + } + + function DeleteConfigTxtFiles() + { + Get-ChildItem $(GetConfigTestDataPath) -Filter Configure*.txt -Recurse | ForEach-Object { Remove-Item $_ } + } + + function GetConfigTestDataFile([string] $fileName) + { + $path = Join-Path $(GetConfigTestDataPath) $fileName + + if (-not (Test-Path $path)) + { + throw "$path does not exists" + } + + return $path + } + + enum TestModuleLocation + { + CurrentUser + AllUsers + Custom + DefaultLocation + } + + function GetExpectedModulePath([TestModuleLocation]$testModuleLocation) + { + switch ($testModuleLocation) + { + ([TestModuleLocation]::CurrentUser) + { + $path = [Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments) + return Join-Path $path "PowerShell\Modules" + } + ([TestModuleLocation]::AllUsers) + { + $path = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles) + return Join-Path $path "PowerShell\Modules" + } + ([TestModuleLocation]::DefaultLocation) + { + $path = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) + return Join-Path $path "Microsoft\WinGet\Configuration\Modules" + } + ([TestModuleLocation]::Custom) + { + return Join-Path $env:TEMP "E2EPesterCustomModules" + } + default + { + throw $testModuleLocation + } + } + } + + function CleanupPsModulePath() + { + $wingetPath = GetExpectedModulePath DefaultLocation + $customPath = GetExpectedModulePath Custom + $modulePath = $env:PsModulePath + $newModulePath = ($modulePath.Split(';') | Where-Object { $_ -ne $wingetPath } | Where-Object { $_ -ne $customPath }) -join ';' + $env:PsModulePath = $newModulePath + } + + function EnsureModuleState([string]$moduleName, [bool]$present, [string]$repository = $null, [TestModuleLocation]$testModuleLocation = [TestModuleLocation]::CurrentUser) + { + CleanupPsModulePath + $wingetPath = GetExpectedModulePath DefaultLocation + $customPath = GetExpectedModulePath Custom + $env:PsModulePath += ";$wingetPath;$customPath" + + $availableModules = Get-Module $moduleName -ListAvailable + $isPresent = $null -ne $availableModules + + if ($isPresent) + { + foreach ($module in $availableModules) + { + try + { + $item = Get-Item $module.Path -ErrorAction Stop + while ($item.Name -ne $moduleName) + { + $item = Get-Item $item.PSParentPath -ErrorAction Stop + } + + if (-not $present) + { + Get-ChildItem $item.FullName -Recurse | Remove-Item -Force -Recurse -ErrorAction Stop + Remove-Item $item -ErrorAction Stop + } + else + { + # Must be in the right location + $expected = GetExpectedModulePath $testModuleLocation + if ($expected -ne $item.Parent.FullName) + { + Get-ChildItem $item.FullName -Recurse | Remove-Item -Force -Recurse -ErrorAction Stop + Remove-Item $item -ErrorAction Stop + $isPresent = $false + } + } + } + catch [System.Management.Automation.ItemNotFoundException] + { + Write-Host "Item not found, ignoring..." $_.Exception.Message + } + } + } + + if ((-not $isPresent) -and $present) + { + $params = @{ + Name = $moduleName + Force = $true + } + + if (-not [string]::IsNullOrEmpty($repository)) + { + $params.Add('Repository', $repository) + } + + if (($testModuleLocation -eq [TestModuleLocation]::CurrentUser) -or + ($testModuleLocation -eq [TestModuleLocation]::AllUsers)) + { + if ($testModuleLocation -eq [TestModuleLocation]::AllUsers) + { + $params.Add('Scope', 'AllUsers') + } + + Install-Module @params + } + else + { + $path = $customPath + if (($testModuleLocation -eq [TestModuleLocation]::WinGetModulePath) -or + ($testModuleLocation -eq [TestModuleLocation]::DefaultLocation)) + { + $path = $wingetPath + } + $params.Add('Path', $path) + + Save-Module @params + } + } + + CleanupPsModulePath + } } Describe 'Test-GroupPolicies' { @@ -103,7 +260,391 @@ Describe 'Test-GroupPolicies' { } } +Describe 'Get configuration' { + + It 'Get configuration and details' { + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $set = Get-WinGetConfigurationDetails -Set $set + $set | Should -Not -BeNullOrEmpty + } + + It 'Get details piped' { + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile | Get-WinGetConfigurationDetails + $set | Should -Not -BeNullOrEmpty + } + + It 'Get configuration and details positional' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $set = Get-WinGetConfigurationDetails $set + $set | Should -Not -BeNullOrEmpty + } + + It 'File doesnt exit' { + $testFile = "c:\dir\fakeFile.txt" + { Get-WinGetConfiguration -File $testFile } | Should -Throw $testFile + } +} + +Describe 'Invoke-WinGetConfiguration' { + + BeforeAll { + DeleteConfigTxtFiles + } + + It 'From Gallery' { + EnsureModuleState "XmlContentDsc" $false + + $testFile = GetConfigTestDataFile "PSGallery_NoModule_NoSettings.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + } + + It 'From TestRepo' { + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + + $expectedModule = Join-Path $(GetExpectedModulePath DefaultLocation) $e2eTestModule + Test-Path $expectedModule | Should -Be $true + } + + It 'From TestRepo Location' -ForEach @( + @{ Location = "CurrentUser"; } + @{ Location = "AllUsers"; } + @{ Location = "DefaultLocation"; } + @{ Location = "Custom"; }) { + $modulePath = "'" + switch ($location) + { + ([TestModuleLocation]::CurrentUser) + { + $modulePath = "currentuser" + break + } + ([TestModuleLocation]::AllUsers) + { + $modulePath = "allusers" + break + } + ([TestModuleLocation]::DefaultLocation) + { + $modulePath = "default" + break + } + ([TestModuleLocation]::Custom) + { + $modulePath = GetExpectedModulePath Custom + break + } + default { + throw $location + } + } + + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo_Location.yml" + $set = Get-WinGetConfiguration -File $testFile -ModulePath $modulePath + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedModule = Join-Path $(GetExpectedModulePath $location) $e2eTestModule + Test-Path $expectedModule | Should -Be $true + } + + It 'Piped' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $result = Get-WinGetConfiguration -File $testFile | Invoke-WinGetConfiguration -AcceptConfigurationAgreements + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Positional' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Independent Resource - One Failure' { + $testFile = GetConfigTestDataFile "IndependentResources_OneFailure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + $result.UnitResults[1].State | Should -Be "Completed" + $result.UnitResults[1].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "IndependentResources_OneFailure.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Dependent Resource - Failure' { + $testFile = GetConfigTestDataFile "DependentResources_Failure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + $result.UnitResults[1].State | Should -Be "Skipped" + $result.UnitResults[1].ResultCode | Should -Be -1978286072 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "DependentResources_Failure.txt" + Test-Path $expectedFile | Should -Be $false + } +} + +Describe 'Start|Complete-WinGetConfiguration' { + + BeforeAll { + DeleteConfigTxtFiles + } + + It 'From Gallery' { + EnsureModuleState "XmlContentDsc" $false + + $testFile = GetConfigTestDataFile "PSGallery_NoModule_NoSettings.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + } + + It 'From TestRepo' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + + # Verify can't be used after. + { Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set } | Should -Throw "Operation is not valid due to the current state of the object." + } + + It 'From TestRepo Location' -ForEach @( + @{ Location = "CurrentUser"; } + @{ Location = "AllUsers"; } + @{ Location = "DefaultLocation"; } + @{ Location = "Custom"; }) { + $modulePath = "'" + switch ($location) + { + ([TestModuleLocation]::CurrentUser) + { + $modulePath = "currentuser" + break + } + ([TestModuleLocation]::AllUsers) + { + $modulePath = "allusers" + break + } + ([TestModuleLocation]::DefaultLocation) + { + $modulePath = "default" + break + } + ([TestModuleLocation]::Custom) + { + $modulePath = GetExpectedModulePath Custom + break + } + default { + throw $location + } + } + + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo_Location.yml" + $set = Get-WinGetConfiguration -File $testFile -ModulePath $modulePath + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedModule = Join-Path $(GetExpectedModulePath $location) $e2eTestModule + Test-Path $expectedModule | Should -Be $true + } + + It 'Piped' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $result = Get-WinGetConfiguration -File $testFile | Start-WinGetConfiguration -AcceptConfigurationAgreements | Complete-WinGetConfiguration + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Positional' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Independent Resource - One Failure' { + $testFile = GetConfigTestDataFile "IndependentResources_OneFailure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + + $result.UnitResults[1].State | Should -Be "Completed" + $result.UnitResults[1].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "IndependentResources_OneFailure.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Dependent Resource - Failure' { + $testFile = GetConfigTestDataFile "DependentResources_Failure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + $result.UnitResults[1].State | Should -Be "Skipped" + $result.UnitResults[1].ResultCode | Should -Be -1978286072 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "DependentResources_Failure.txt" + Test-Path $expectedFile | Should -Be $false + } +} + AfterAll { CleanupGroupPolicies CleanupGroupPolicyKeyIfExists + CleanupPsModulePath + DeleteConfigTxtFiles } \ No newline at end of file