Skip to content

Commit

Permalink
Support VS/.NET CLI localization in MTP, and add our own env variable…
Browse files Browse the repository at this point in the history
… as well (#4122)
  • Loading branch information
Youssef1313 authored Nov 22, 2024
1 parent 8ce1906 commit e4c0836
Show file tree
Hide file tree
Showing 20 changed files with 213 additions and 52 deletions.
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
101 changes: 101 additions & 0 deletions src/Platform/Microsoft.Testing.Platform/Helpers/UILanguageOverride.cs
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,35 @@ 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");
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");
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 @@ -16,7 +16,7 @@ namespace MSTest.Acceptance.IntegrationTests.Messages.V100;
public partial /* for codegen regx */ class ServerModeTestsBase : AcceptanceTestBase
{
private static readonly string Root = RootFinder.Find();
private static readonly Dictionary<string, string> DefaultEnvironmentVariables = new()
private static readonly Dictionary<string, string?> DefaultEnvironmentVariables = new()
{
{ "DOTNET_ROOT", $"{Root}/.dotnet" },
{ "DOTNET_INSTALL_DIR", $"{Root}/.dotnet" },
Expand All @@ -31,7 +31,7 @@ protected ServerModeTestsBase(ITestExecutionContext testExecutionContext)

protected async Task<TestingPlatformClient> StartAsServerAndConnectToTheClientAsync(TestHost testHost)
{
var environmentVariables = new Dictionary<string, string>(DefaultEnvironmentVariables);
var environmentVariables = new Dictionary<string, string?>(DefaultEnvironmentVariables);
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
{
// Skip all unwanted environment variables.
Expand Down Expand Up @@ -80,7 +80,7 @@ protected async Task<TestingPlatformClient> StartAsServerAndConnectToTheClientAs

protected async Task<TestingPlatformClient> StartAsServerAndConnectAsync(TestHost testHost, bool enableDiagnostic = false)
{
var environmentVariables = new Dictionary<string, string>(DefaultEnvironmentVariables);
var environmentVariables = new Dictionary<string, string?>(DefaultEnvironmentVariables);
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
{
// Skip all unwanted environment variables.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public async Task CrashPlusHangDump_InCaseOfCrash_CreateCrashDump()
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, "CrashPlusHangDump", TargetFrameworks.NetCurrent.Arguments);
TestHostResult testHostResult = await testHost.ExecuteAsync(
$"--hangdump --hangdump-timeout 5m --crashdump --results-directory {resultDirectory}",
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ "SLEEPTIMEMS1", "4000" },
{ "SLEEPTIMEMS2", "600000" },
Expand All @@ -41,7 +41,7 @@ public async Task CrashPlusHangDump_InCaseOfHang_CreateHangDump()
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, "CrashPlusHangDump", TargetFrameworks.NetCurrent.Arguments);
TestHostResult testHostResult = await testHost.ExecuteAsync(
$"--hangdump --hangdump-timeout 8s --crashdump --results-directory {resultDirectory}",
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ "SLEEPTIMEMS1", "4000" },
{ "SLEEPTIMEMS2", "600000" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public async Task UsingNoBanner_InTheEnvironmentVars_TheBannerDoesNotAppear(stri
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, AssetName, tfm);
TestHostResult testHostResult = await testHost.ExecuteAsync(
null,
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ "TESTINGPLATFORM_NOBANNER", "true" },
});
Expand All @@ -46,7 +46,7 @@ public async Task UsingDotnetNoLogo_InTheEnvironmentVars_TheBannerDoesNotAppear(
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, AssetName, tfm);
TestHostResult testHostResult = await testHost.ExecuteAsync(
null,
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ "DOTNET_NOLOGO", "true" },
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public async Task Diag_EnableWithEnvironmentVariables_Succeeded(string tfm)
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, AssetName, tfm);
TestHostResult testHostResult = await testHost.ExecuteAsync(
null,
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC, "1" },
});
Expand All @@ -127,7 +127,7 @@ public async Task Diag_EnableWithEnvironmentVariables_Verbosity_Succeeded(string
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, AssetName, tfm);
TestHostResult testHostResult = await testHost.ExecuteAsync(
null,
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC, "1" },
{ EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC_VERBOSITY, "Trace" },
Expand All @@ -145,7 +145,7 @@ public async Task Diag_EnableWithEnvironmentVariables_CustomPrefix_Succeeded(str
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, AssetName, tfm);
TestHostResult testHostResult = await testHost.ExecuteAsync(
null,
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC, "1" },
{ EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC_OUTPUT_FILEPREFIX, "MyPrefix" },
Expand All @@ -163,7 +163,7 @@ public async Task Diag_EnableWithEnvironmentVariables_SynchronousWrite_Succeeded
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, AssetName, tfm);
TestHostResult testHostResult = await testHost.ExecuteAsync(
null,
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC, "1" },
{ EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC_FILELOGGER_SYNCHRONOUSWRITE, "1" },
Expand All @@ -179,7 +179,7 @@ public async Task Diag_EnableWithEnvironmentVariables_Disable_Succeeded(string t

TestHostResult testHostResult = await testHost.ExecuteAsync(
"--diagnostic",
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC, "0" },
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public void ExitOnProcessExit_Succeed(string tfm)

// Create the mutex name used to wait for the PID file created by the test host.
string waitPid = Guid.NewGuid().ToString("N");
_ = testHost.ExecuteAsync(environmentVariables: new Dictionary<string, string> { { "WaitPid", waitPid } });
_ = testHost.ExecuteAsync(environmentVariables: new Dictionary<string, string?> { { "WaitPid", waitPid } });

Process? process;
var startTime = Stopwatch.StartNew();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public async Task HangDump_Outputs_HangingTests_EvenWhenHangingTestsHaveTheSameD
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, "HangDump", TargetFrameworks.NetCurrent.Arguments);
TestHostResult testHostResult = await testHost.ExecuteAsync(
$"--hangdump --hangdump-timeout 8s --hangdump-type {format} --results-directory {resultDirectory} --no-progress",
new Dictionary<string, string>
new Dictionary<string, string?>
{
{ "SLEEPTIMEMS1", "100" },
{ "SLEEPTIMEMS2", "600000" },
Expand Down
Loading

0 comments on commit e4c0836

Please sign in to comment.