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

Support VS/.NET CLI localization in MTP, and add our own env variable as well #4122

Merged
merged 7 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -71,13 +71,17 @@ public static Task<ITestApplicationBuilder> CreateServerModeBuilderAsync(string[
/// <returns>The task representing the asynchronous operation.</returns>
public static async Task<ITestApplicationBuilder> CreateBuilderAsync(string[] args, TestApplicationOptions? testApplicationOptions = null)
{
SystemEnvironment systemEnvironment = new();

// See AB#2304879.
UILanguageOverride.SetCultureSpecifiedByUser(systemEnvironment);

// We get the time to save it in the logs for testcontrollers troubleshooting.
SystemClock systemClock = new();
DateTimeOffset createBuilderStart = systemClock.UtcNow;
string createBuilderEntryTime = createBuilderStart.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
testApplicationOptions ??= new TestApplicationOptions();

SystemEnvironment systemEnvironment = new();
LaunchAttachDebugger(systemEnvironment);

// First step is to parse the command line from where we get the second input layer.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Globalization;

using Microsoft.Testing.Platform.Helpers;

namespace Microsoft.Testing.Platform;

// Borrowed from dotnet/sdk with some tweaks to allow testing
internal static class UILanguageOverride
{
#pragma warning disable SA1310 // Field names should not contain underscore - That's how we want to name the environment variables!
private const string TESTINGPLATFORM_UI_LANGUAGE = nameof(TESTINGPLATFORM_UI_LANGUAGE);
private const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE);
#pragma warning restore SA1310 // Field names should not contain underscore

private const string VSLANG = nameof(VSLANG);
private const string PreferredUILang = nameof(PreferredUILang);

internal static void SetCultureSpecifiedByUser(IEnvironment environment)
{
CultureInfo? language = GetOverriddenUILanguage(environment);
if (language == null)
{
return;
}

ApplyOverrideToCurrentProcess(language);
FlowOverrideToChildProcesses(language, environment);
}

private static void ApplyOverrideToCurrentProcess(CultureInfo language)
=> CultureInfo.DefaultThreadCurrentUICulture = language;

private static CultureInfo? GetOverriddenUILanguage(IEnvironment environment)
{
// For MTP, TESTINGPLATFORM_UI_LANGUAGE environment variable is the highest precedence.
string? testingPlatformLanguage = environment.GetEnvironmentVariable(TESTINGPLATFORM_UI_LANGUAGE);
if (testingPlatformLanguage is not null)
{
try
{
return CultureInfo.GetCultureInfo(testingPlatformLanguage);
}
catch (CultureNotFoundException)
{
}
}

// If TESTINGPLATFORM_UI_LANGUAGE is not set or is invalid, then DOTNET_CLI_UI_LANGUAGE=<culture name> is the main way for users to customize the CLI's UI language.
string? dotnetCliLanguage = environment.GetEnvironmentVariable(DOTNET_CLI_UI_LANGUAGE);
if (dotnetCliLanguage is not null)
{
try
{
return new CultureInfo(dotnetCliLanguage);
}
catch (CultureNotFoundException)
{
}
}

// VSLANG=<lcid> is set by VS and we respect that as well so that we will respect the VS
// language preference if we're invoked by VS.
string? vsLang = environment.GetEnvironmentVariable(VSLANG);
if (vsLang != null && int.TryParse(vsLang, out int vsLcid))
{
try
{
return new CultureInfo(vsLcid);
}
catch (ArgumentOutOfRangeException)
{
}
catch (CultureNotFoundException)
{
}
}

return null;
}

private static void FlowOverrideToChildProcesses(CultureInfo language, IEnvironment environment)
{
// Do not override any environment variables that are already set as we do not want to clobber a more granular setting with our global setting.
SetIfNotAlreadySet(TESTINGPLATFORM_UI_LANGUAGE, language.Name, environment);
SetIfNotAlreadySet(DOTNET_CLI_UI_LANGUAGE, language.Name, environment);
SetIfNotAlreadySet(VSLANG, language.LCID.ToString(CultureInfo.CurrentCulture), environment); // for tools following VS guidelines to just work in CLI
SetIfNotAlreadySet(PreferredUILang, language.Name, environment); // for C#/VB targets that pass $(PreferredUILang) to compiler
}

private static void SetIfNotAlreadySet(string environmentVariableName, string value, IEnvironment environment)
{
string? currentValue = environment.GetEnvironmentVariable(environmentVariableName);
if (currentValue == null)
{
environment.SetEnvironmentVariable(environmentVariableName, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Globalization;
using System.Runtime.InteropServices;

using Microsoft.Testing.Platform.Acceptance.IntegrationTests;
using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers;
using Microsoft.Testing.Platform.Helpers;

namespace MSTest.Acceptance.IntegrationTests;

Expand All @@ -22,6 +24,27 @@ internal static IEnumerable<TestArgumentsEntry<string>> TfmList()
yield return TargetFrameworks.NetFramework.First();
}

internal static IEnumerable<TestArgumentsEntry<(string? TestingPlatformUILanguage, string? DotnetCLILanguage, string? VSLang, string ExpectedLocale)>> LocalizationTestCases()
{
// Show that TestingPlatformUILanguage is respected.
yield return new TestArgumentsEntry<(string?, string?, string?, string)>(("fr-FR", null, null, "fr-FR"), "TestingPlatformUILanguage: fr-FR, expected: fr-FR");

// Show that TestingPlatformUILanguage takes precedence over DotnetCLILanguage.
yield return new TestArgumentsEntry<(string?, string?, string?, string)>(("fr-FR", "it-IT", null, "fr-FR"), "TestingPlatformUILanguage: fr-FR, CLI: it-IT, expected: fr-FR");

// Show that DotnetCLILanguage is respected.
yield return new TestArgumentsEntry<(string?, string?, string?, string)>((null, "it-IT", null, "it-IT"), "CLI: it-IT, expected: it-IT");

// Show that DotnetCLILanguage takes precedence over VSLang.
yield return new TestArgumentsEntry<(string?, string?, string?, string)>((null, "it-IT", "fr-FR", "it-IT"), "CLI: it-IT, VSLang: fr-FR, expected: it-IT");

// Show that VSLang is respected.
yield return new TestArgumentsEntry<(string?, string?, string?, string)>((null, null, "it-IT", "it-IT"), "VSLang: it-IT, expected: it-IT");

// Show that TestingPlatformUILanguage takes precedence over everything.
yield return new TestArgumentsEntry<(string?, string?, string?, string)>(("fr-FR", "it-IT", "it-IT", "fr-FR"), "TestingPlatformUILanguage: fr-FR, CLI: it-IT, VSLang: it-IT, expected: fr-FR");
}

[ArgumentsProvider(nameof(TfmList))]
public async Task UnsupportedRunSettingsEntriesAreFlagged(string tfm)
{
Expand All @@ -48,6 +71,49 @@ public async Task UnsupportedRunSettingsEntriesAreFlagged(string tfm)
testHostResult.AssertOutputContains("Runsettings attribute 'TreatNoTestsAsError' is not supported by Microsoft.Testing.Platform and will be ignored");
}

[ArgumentsProvider(nameof(LocalizationTestCases))]
public async Task UnsupportedRunSettingsEntriesAreFlagged_Localization((string? TestingPlatformUILanguage, string? DotnetCLILanguage, string? VSLang, string? ExpectedLocale) testArgument)
{
var testHost = TestHost.LocateFrom(_testAssetFixture.ProjectPath, TestAssetFixture.ProjectName, TargetFrameworks.NetCurrent.Arguments);
TestHostResult testHostResult = await testHost.ExecuteAsync("--settings my.runsettings", environmentVariables: new()
{
["TESTINGPLATFORM_UI_LANGUAGE"] = testArgument.TestingPlatformUILanguage,
["DOTNET_CLI_UI_LANGUAGE"] = testArgument.DotnetCLILanguage,
["VSLANG"] = testArgument.VSLang is null ? null : new CultureInfo(testArgument.VSLang).LCID.ToString(CultureInfo.CurrentCulture),
});

// Assert
testHostResult.AssertExitCodeIs(0);

switch (testArgument.ExpectedLocale)
{
case "fr-FR":
testHostResult.AssertOutputContains("Les loggers Runsettings ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
testHostResult.AssertOutputContains("Les datacollecteurs Runsettings ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
testHostResult.AssertOutputContains("Les attributs Runsettings « MaxCpuCount » ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
testHostResult.AssertOutputContains("Les attributs Runsettings « TargetFrameworkVersion » ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
testHostResult.AssertOutputContains("Les attributs Runsettings « TargetPlatform » ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
testHostResult.AssertOutputContains("Les attributs Runsettings « TestAdaptersPaths » ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
testHostResult.AssertOutputContains("Les attributs Runsettings « TestCaseFilter » ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
testHostResult.AssertOutputContains("Les attributs Runsettings « TestSessionTimeout » ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
testHostResult.AssertOutputContains("Les attributs Runsettings « TreatNoTestsAsError » ne sont pas pris en charge par Microsoft.Testing.Platform et seront ignorés");
break;
case "it-IT":
testHostResult.AssertOutputContains("I logger Runsettings non sono supportati da Microsoft.Testing.Platform e verranno ignorati");
testHostResult.AssertOutputContains("I datacollector Runsettings non sono supportati da Microsoft.Testing.Platform e verranno ignorati");
testHostResult.AssertOutputContains("L'attributo Runsettings `MaxCpuCount' non è supportato da Microsoft.Testing.Platform e verrà ignorato");
testHostResult.AssertOutputContains("L'attributo Runsettings `TargetFrameworkVersion' non è supportato da Microsoft.Testing.Platform e verrà ignorato");
testHostResult.AssertOutputContains("L'attributo Runsettings `TargetPlatform' non è supportato da Microsoft.Testing.Platform e verrà ignorato");
testHostResult.AssertOutputContains("L'attributo Runsettings `TestAdaptersPaths' non è supportato da Microsoft.Testing.Platform e verrà ignorato");
testHostResult.AssertOutputContains("L'attributo Runsettings `TestCaseFilter' non è supportato da Microsoft.Testing.Platform e verrà ignorato");
testHostResult.AssertOutputContains("L'attributo Runsettings `TestSessionTimeout' non è supportato da Microsoft.Testing.Platform e verrà ignorato");
testHostResult.AssertOutputContains("L'attributo Runsettings `TreatNoTestsAsError' non è supportato da Microsoft.Testing.Platform e verrà ignorato");
break;
default:
throw ApplicationStateGuard.Unreachable();
}
}

[TestFixture(TestFixtureSharingStrategy.PerTestGroup)]
public sealed class TestAssetFixture(AcceptanceFixture acceptanceFixture) : TestAssetFixtureBase(acceptanceFixture.NuGetGlobalPackagesFolder)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static int MaxOutstandingCommands

public async Task RunAsync(
string commandLine,
IDictionary<string, string>? environmentVariables = null)
IDictionary<string, string?>? environmentVariables = null)
{
int exitCode = await RunAsyncAndReturnExitCodeAsync(commandLine, environmentVariables);
if (exitCode != 0)
Expand All @@ -57,7 +57,7 @@ public async Task RunAsync(

public async Task<int> RunAsyncAndReturnExitCodeAsync(
string commandLine,
IDictionary<string, string>? environmentVariables = null,
IDictionary<string, string?>? environmentVariables = null,
string? workingDirectory = null,
bool cleanDefaultEnvironmentVariableIfCustomAreProvided = false,
int timeoutInSeconds = 60)
Expand Down
30 changes: 15 additions & 15 deletions test/Utilities/Microsoft.Testing.TestInfrastructure/DotnetMuxer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace Microsoft.Testing.TestInfrastructure;
public class DotnetMuxer : IDisposable
{
private static readonly string Root = RootFinder.Find();
private static readonly IDictionary<string, string> DefaultEnvironmentVariables
= new Dictionary<string, string>
private static readonly IDictionary<string, string?> DefaultEnvironmentVariables
= new Dictionary<string, string?>
{
{ "DOTNET_ROOT", $"{Root}/.dotnet" },
{ "DOTNET_INSTALL_DIR", $"{Root}/.dotnet" },
Expand All @@ -18,21 +18,21 @@ private static readonly IDictionary<string, string> DefaultEnvironmentVariables
};

private readonly string _dotnet;
private readonly IDictionary<string, string> _environmentVariables;
private readonly IDictionary<string, string?> _environmentVariables;
private readonly CommandLine _commandLine;
private bool _isDisposed;

public DotnetMuxer()
: this(
DefaultEnvironmentVariables,
new Dictionary<string, string>(),
new Dictionary<string, string?>(),
mergeDefaultEnvironmentVariables: true,
useDefaultArtifactsPackages: true)
{
}

public DotnetMuxer(
IDictionary<string, string> environmentVariables,
IDictionary<string, string?> environmentVariables,
bool mergeEnvironmentVariables = true,
bool useDefaultArtifactPackages = true)
: this(
Expand All @@ -44,8 +44,8 @@ public DotnetMuxer(
}

private DotnetMuxer(
IDictionary<string, string> defaultEnvironmentVariables,
IDictionary<string, string> environmentVariables,
IDictionary<string, string?> defaultEnvironmentVariables,
IDictionary<string, string?> environmentVariables,
bool mergeDefaultEnvironmentVariables = true,
bool useDefaultArtifactsPackages = true)
{
Expand Down Expand Up @@ -98,7 +98,7 @@ public async Task<int> ExecuteAsync(string arguments, string? workingDirectory =
public async Task<int> ExecuteAsync(
string arguments,
string? workingDirectory,
IDictionary<string, string> environmentVariables,
IDictionary<string, string?> environmentVariables,
int timeoutInSeconds = 60)
=> await _commandLine.RunAsyncAndReturnExitCodeAsync(
$"{_dotnet} {arguments}",
Expand All @@ -107,22 +107,22 @@ public async Task<int> ExecuteAsync(
cleanDefaultEnvironmentVariableIfCustomAreProvided: true,
timeoutInSeconds: timeoutInSeconds);

private IDictionary<string, string> MergeEnvironmentVariables(
IDictionary<string, string> environmentVariables1,
IDictionary<string, string> environmentVariables2)
private IDictionary<string, string?> MergeEnvironmentVariables(
IDictionary<string, string?> environmentVariables1,
IDictionary<string, string?> environmentVariables2)
{
if (environmentVariables1.Count == 0)
{
return new Dictionary<string, string>(environmentVariables2);
return new Dictionary<string, string?>(environmentVariables2);
}

if (environmentVariables2.Count == 0)
{
return new Dictionary<string, string>(environmentVariables1);
return new Dictionary<string, string?>(environmentVariables1);
}

IDictionary<string, string> mergedEnvironmentVariables = new Dictionary<string, string>(environmentVariables1);
foreach (KeyValuePair<string, string> kvp in environmentVariables2)
IDictionary<string, string?> mergedEnvironmentVariables = new Dictionary<string, string?>(environmentVariables1);
foreach (KeyValuePair<string, string?> kvp in environmentVariables2)
{
mergedEnvironmentVariables[kvp.Key] = kvp.Value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public sealed class ProcessConfiguration

public string? WorkingDirectory { get; init; }

public IDictionary<string, string>? EnvironmentVariables { get; init; }
public IDictionary<string, string?>? EnvironmentVariables { get; init; }

public Action<IProcessHandle, string>? OnErrorOutput { get; init; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static IProcessHandle Start(ProcessConfiguration config, bool cleanDefaul
processStartInfo.EnvironmentVariables.Clear();
}

foreach (KeyValuePair<string, string> kvp in config.EnvironmentVariables)
foreach (KeyValuePair<string, string?> kvp in config.EnvironmentVariables)
{
if (kvp.Value is null)
{
Expand Down
10 changes: 7 additions & 3 deletions test/Utilities/Microsoft.Testing.TestInfrastructure/TestHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static int MaxOutstandingExecutions

public async Task<TestHostResult> ExecuteAsync(
string? command = null,
Dictionary<string, string>? environmentVariables = null,
Dictionary<string, string?>? environmentVariables = null,
bool disableTelemetry = true,
int timeoutSeconds = 60)
{
Expand All @@ -55,7 +55,7 @@ public async Task<TestHostResult> ExecuteAsync(
throw new InvalidOperationException($"Command should not start with module name '{_testHostModuleName}'.");
}

environmentVariables ??= new Dictionary<string, string>();
environmentVariables ??= new Dictionary<string, string?>();

if (disableTelemetry)
{
Expand All @@ -71,7 +71,11 @@ public async Task<TestHostResult> ExecuteAsync(
continue;
}

environmentVariables.Add(key!, entry.Value!.ToString()!);
// We use TryAdd to let tests "overwrite" existing environment variables.
// Consider that the given dictionary has "TESTINGPLATFORM_UI_LANGUAGE" as a key.
// And also Environment.GetEnvironmentVariables() is returning TESTINGPLATFORM_UI_LANGUAGE.
// In that case, we do a "TryAdd" which effectively means the value from the original dictionary wins.
environmentVariables.TryAdd(key!, entry!.Value!.ToString()!);
}

// Define DOTNET_ROOT to point to the dotnet we install for this repository, to avoid
Expand Down
Loading