From 270f8f1be7cb5e1b0739ec82f45ffa43c2bda40c Mon Sep 17 00:00:00 2001 From: Dominik Kaszewski Date: Tue, 9 May 2023 18:46:59 +0200 Subject: [PATCH] Allow partial culture matching in `Update-Help` (#18037) --- .../engine/Utils.cs | 5 +- .../help/UpdatableHelpCommandBase.cs | 5 +- .../help/UpdatableHelpInfo.cs | 58 ++++++++++++++----- .../help/UpdatableHelpSystem.cs | 20 ++----- .../engine/Help/HelpSystem.Tests.ps1 | 53 ++++++++++++++++- 5 files changed, 108 insertions(+), 33 deletions(-) diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index a6642e81c5..c70eb34cdf 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -1514,11 +1514,14 @@ namespace System.Management.Automation.Internal internal static bool UseDebugAmsiImplementation; internal static bool BypassAppLockerPolicyCaching; internal static bool BypassOnlineHelpRetrieval; - internal static bool ThrowHelpCultureNotSupported; internal static bool ForcePromptForChoiceDefaultOption; internal static bool NoPromptForPassword; internal static bool ForceFormatListFixedLabelWidth; + // Update-Help tests + internal static bool ThrowHelpCultureNotSupported; + internal static CultureInfo CurrentUICulture; + // Stop/Restart/Rename Computer tests internal static bool TestStopComputer; internal static bool TestWaitStopComputer; diff --git a/src/System.Management.Automation/help/UpdatableHelpCommandBase.cs b/src/System.Management.Automation/help/UpdatableHelpCommandBase.cs index b759a281bd..48e44bd301 100644 --- a/src/System.Management.Automation/help/UpdatableHelpCommandBase.cs +++ b/src/System.Management.Automation/help/UpdatableHelpCommandBase.cs @@ -557,7 +557,8 @@ namespace Microsoft.PowerShell.Commands #endif catch (UpdatableHelpSystemException e) { - if (e.FullyQualifiedErrorId == "HelpCultureNotSupported") + if (e.FullyQualifiedErrorId == "HelpCultureNotSupported" + || e.FullyQualifiedErrorId == "UnableToRetrieveHelpInfoXml") { installed = false; @@ -672,7 +673,7 @@ namespace Microsoft.PowerShell.Commands } // Culture check - if (!newHelpInfo.IsCultureSupported(culture)) + if (!newHelpInfo.IsCultureSupported(culture.Name)) { throw new UpdatableHelpSystemException("HelpCultureNotSupported", StringUtil.Format(HelpDisplayStrings.HelpCultureNotSupported, diff --git a/src/System.Management.Automation/help/UpdatableHelpInfo.cs b/src/System.Management.Automation/help/UpdatableHelpInfo.cs index 5af3fd57b6..8170de9065 100644 --- a/src/System.Management.Automation/help/UpdatableHelpInfo.cs +++ b/src/System.Management.Automation/help/UpdatableHelpInfo.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; +using System.Linq; using System.Management.Automation.Internal; using System.Text; @@ -37,6 +39,44 @@ namespace System.Management.Automation.Help /// Supported culture. /// internal CultureInfo Culture { get; set; } + + /// + /// Enumerates fallback chain (parents) of the culture, including itself. + /// + /// Culture to enumerate + /// + /// Examples: + /// en-GB => { en-GB, en } + /// zh-Hans-CN => { zh-Hans-CN, zh-Hans, zh }. + /// + /// An enumerable list of culture names. + internal static IEnumerable GetCultureFallbackChain(CultureInfo culture) + { + // We use just names instead because comparing two CultureInfo objects + // can fail if they are created using different means + while (culture != null) + { + if (string.IsNullOrEmpty(culture.Name)) + { + yield break; + } + + yield return culture.Name; + + culture = culture.Parent; + } + } + + /// + /// Checks if a culture is supported. + /// + /// Name of the culture to check. + /// True if supported, false if not. + internal bool IsCultureSupported(string cultureName) + { + Debug.Assert(cultureName != null, $"{nameof(cultureName)} may not be null"); + return GetCultureFallbackChain(Culture).Any(fallback => fallback == cultureName); + } } /// @@ -99,22 +139,12 @@ namespace System.Management.Automation.Help /// /// Checks if a culture is supported. /// - /// Culture to check. + /// Name of the culture to check. /// True if supported, false if not. - internal bool IsCultureSupported(CultureInfo culture) + internal bool IsCultureSupported(string cultureName) { - Debug.Assert(culture != null); - - foreach (CultureSpecificUpdatableHelp updatableHelpItem in UpdatableHelpItems) - { - if (string.Equals(updatableHelpItem.Culture.Name, culture.Name, - StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; + Debug.Assert(cultureName != null, $"{nameof(cultureName)} may not be null"); + return UpdatableHelpItems.Any(item => item.IsCultureSupported(cultureName)); } /// diff --git a/src/System.Management.Automation/help/UpdatableHelpSystem.cs b/src/System.Management.Automation/help/UpdatableHelpSystem.cs index eb9796a207..3a504a37fd 100644 --- a/src/System.Management.Automation/help/UpdatableHelpSystem.cs +++ b/src/System.Management.Automation/help/UpdatableHelpSystem.cs @@ -294,19 +294,13 @@ namespace System.Management.Automation.Help { CultureInfo culture = CultureInfo.CurrentUICulture; - while (culture != null) + // Allow tests to override system culture + if (InternalTestHooks.CurrentUICulture != null) { - if (string.IsNullOrEmpty(culture.Name)) - { - yield break; - } - - yield return culture.Name; - - culture = culture.Parent; + culture = InternalTestHooks.CurrentUICulture; } - yield break; + return CultureSpecificUpdatableHelp.GetCultureFallbackChain(culture); } #region Help Metadata Retrieval @@ -594,13 +588,9 @@ namespace System.Management.Automation.Help if (!string.IsNullOrEmpty(currentCulture)) { - IEnumerable patternList = SessionStateUtilities.CreateWildcardsFromStrings( - globPatterns: new[] { currentCulture }, - options: WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); - for (int i = 0; i < updatableHelpItem.Length; i++) { - if (SessionStateUtilities.MatchesAnyWildcardPattern(updatableHelpItem[i].Culture.Name, patternList, true)) + if (updatableHelpItem[i].IsCultureSupported(currentCulture)) { helpInfo.HelpContentUriCollection.Add(new UpdatableHelpUri(moduleName, moduleGuid, updatableHelpItem[i].Culture, uri)); } diff --git a/test/powershell/engine/Help/HelpSystem.Tests.ps1 b/test/powershell/engine/Help/HelpSystem.Tests.ps1 index 2786e64ee0..53ce9ad749 100644 --- a/test/powershell/engine/Help/HelpSystem.Tests.ps1 +++ b/test/powershell/engine/Help/HelpSystem.Tests.ps1 @@ -30,7 +30,7 @@ function UpdateHelpFromLocalContentPath { throw "Unable to find help content at '$helpContentPath'" } - # Test files are 'en-US', set explicit culture so test does not help on non-US systems + # Test files are 'en-US', set explicit culture so test does not fail on non-US systems Update-Help -Module $ModuleName -SourcePath $helpContentPath -UICulture 'en-US' -Force -ErrorAction Stop -Scope $Scope } @@ -635,3 +635,54 @@ Describe 'help renders when using a PAGER with a space in the path' -Tags 'CI' { help Get-Command | Should -Be "R2V0LUNvbW1hbmQ=" } } + +Describe 'Update-Help allows partial culture matches' -Tags 'CI' { + BeforeAll { + function Test-UpdateHelpAux($UICulture, $Pass) + { + # If null (in system culture tests), omit entirely + $CultureArg = $UICulture ? @{ UICulture = $UICulture } : @{} + $Args = @{ + Module = 'Microsoft.PowerShell.Core' + SourcePath = Join-Path $PSScriptRoot 'assets' + Force = $true + ErrorAction = $Pass ? 'Stop' : 'SilentlyContinue' + ErrorVariable = 'ErrorVariable' + } + + Update-Help @Args @CultureArg + + if (-not $Pass) { + $ErrorVariable | Should -Match 'Failed to update Help for the module.*' + } + } + } + + AfterEach { + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('CurrentUICulture', $null) + } + + It 'Checks culture match against en-US: ' -TestCases @( + @{ UICulture = 'en-US' } + @{ UICulture = 'en' } + @{ UICulture = 'en-GB'; Pass = $false } + @{ UICulture = 'de-DE'; Pass = $false } + ) { + param($UICulture, $Pass = $true) + + Test-UpdateHelpAux $UICulture $Pass + } + + # When using system culture, "en-GB" will use "en" as fallback, so passes + It 'Checks system culture match against en-US: ' -TestCases @( + @{ UICulture = 'en-US' } + @{ UICulture = 'en' } + @{ UICulture = 'en-GB' } + @{ UICulture = 'de-DE'; Pass = $false } + ) { + param($UICulture, $Pass = $true) + + [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('CurrentUICulture', [System.Globalization.CultureInfo]::new($UICulture)) + Test-UpdateHelpAux $null $Pass + } +}