Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix locale-sensitive comparison of UnmatchedArgument error strings #29746

Merged
merged 7 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
using System;
using System.Globalization;

namespace Microsoft.DotNet.Cli
namespace Microsoft.DotNet.Cli.Utils
{
internal static class UILanguageOverride
{
private const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE);
internal const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE);
private const string VSLANG = nameof(VSLANG);
private const string PreferredUILang = nameof(PreferredUILang);

Expand Down
43 changes: 37 additions & 6 deletions src/Cli/dotnet/ParseResultExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.DotNet.Cli.Utils;
using static Microsoft.DotNet.Cli.Parser;

Expand Down Expand Up @@ -34,21 +35,51 @@ public static void ShowHelp(this ParseResult parseResult)
Parser.Instance.Parse(tokenList).Invoke();
}


public static void ShowHelpOrErrorIfAppropriate(this ParseResult parseResult)
{
if (parseResult.Errors.Any())
{
var unrecognizedTokenErrors = parseResult.Errors.Where(error =>
error.Message.Contains(Parser.Instance.Configuration.LocalizationResources.UnrecognizedCommandOrArgument(string.Empty).Replace("'", string.Empty)));
{
// Can't really cache this access in a static or something because it implicitly depends on the environment.
var rawResourcePartsForThisLocale = DistinctFormatStringParts(CommandLineValidation.LocalizableStrings.UnrecognizedCommandOrArgument);
return ErrorContainsAllParts(error.Message, rawResourcePartsForThisLocale);
});
if (parseResult.CommandResult.Command.TreatUnmatchedTokensAsErrors ||
parseResult.Errors.Except(unrecognizedTokenErrors).Any())
{
throw new CommandParsingException(
message: string.Join(Environment.NewLine,
parseResult.Errors.Select(e => e.Message)),
parseResult.Errors.Select(e => e.Message)),
parseResult: parseResult);
}
}

///<summary>Splits a .NET format string by the format placeholders (the {N} parts) to get an array of the literal parts, to be used in message-checking</summary>
static string[] DistinctFormatStringParts(string formatString)
{
return Regex.Split(formatString, @"{[0-9]+}"); // match the literal '{', followed by any of 0-9 one or more times, followed by the literal '}'
}


/// <summary>given a string and a series of parts, ensures that all parts are present in the string in sequential order</summary>
static bool ErrorContainsAllParts(ReadOnlySpan<char> error, string[] parts)
{
foreach(var part in parts) {
var foundIndex = error.IndexOf(part);
if (foundIndex != -1)
{
error = error.Slice(foundIndex + part.Length);
continue;
}
else
{
return false;
}
}
return true;
}
}

public static string RootSubCommandResult(this ParseResult parseResult)
Expand All @@ -60,7 +91,7 @@ public static string RootSubCommandResult(this ParseResult parseResult)

public static bool IsDotnetBuiltInCommand(this ParseResult parseResult)
{
return string.IsNullOrEmpty(parseResult.RootSubCommandResult()) ||
return string.IsNullOrEmpty(parseResult.RootSubCommandResult()) ||
Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null;
}

Expand Down Expand Up @@ -129,7 +160,7 @@ private static string GetSymbolResultValue(ParseResult parseResult, SymbolResult

public static bool BothArchAndOsOptionsSpecified(this ParseResult parseResult) =>
(parseResult.HasOption(CommonOptions.ArchitectureOption) ||
parseResult.HasOption(CommonOptions.LongFormArchitectureOption)) &&
parseResult.HasOption(CommonOptions.LongFormArchitectureOption)) &&
parseResult.HasOption(CommonOptions.OperatingSystemOption);

internal static string GetCommandLineRuntimeIdentifier(this ParseResult parseResult)
Expand Down Expand Up @@ -206,7 +237,7 @@ public static object SafelyGetValueForOption(this ParseResult parseResult, Optio
!parseResult.Errors.Any(e => e.SymbolResult == optionResult))
{
return optionResult.GetValueForOption(optionToGet);
}
}
else {
return default;
}
Expand All @@ -224,7 +255,7 @@ public static T SafelyGetValueForOption<T>(this ParseResult parseResult, Option<
!parseResult.Errors.Any(e => e.SymbolResult == optionResult))
{
return optionResult.GetValueForOption(optionToGet);
}
}
else {
return default;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Tests/Microsoft.NET.TestFramework/Commands/TestCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public TestCommand WithWorkingDirectory(string workingDirectory)
return this;
}

public TestCommand WithCulture(string locale) => WithEnvironmentVariable(UILanguageOverride.DOTNET_CLI_UI_LANGUAGE, locale);

public TestCommand WithTraceOutput()
{
WithEnvironmentVariable("DOTNET_CLI_VSTEST_TRACE", "1");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.DotNet.Tools.Test.Utilities;
using Xunit;
using FluentAssertions;
using Microsoft.DotNet.Cli.Utils;
using System.IO;
using System;
using Microsoft.NET.TestFramework;
using Microsoft.NET.TestFramework.Assertions;
using Microsoft.NET.TestFramework.Commands;
using Xunit.Abstractions;

namespace Microsoft.DotNet.Cli.Test.Tests;

public class CultureAwareTestProject : SdkTest
{
private const string TestAppName = "TestAppSimple";

public CultureAwareTestProject(ITestOutputHelper log) : base(log)
{
}

[InlineData("en-US")]
[InlineData("de-DE")]
[Theory]
public void CanRunTestsAgainstProjectInLocale(string locale)
{
var testAsset = _testAssetsManager.CopyTestAsset(TestAppName)
.WithSource()
.WithVersionVariables();

var command = new DotnetTestCommand(Log).WithWorkingDirectory(testAsset.Path).WithCulture(locale);
var result = command.Execute();

result.ExitCode.Should().Be(0);
}
}