diff --git a/src/Microsoft.TestPlatform.CoreUtilities/Constants.cs b/src/Microsoft.TestPlatform.CoreUtilities/Constants.cs index 1af6ef5371..e4868a75fe 100644 --- a/src/Microsoft.TestPlatform.CoreUtilities/Constants.cs +++ b/src/Microsoft.TestPlatform.CoreUtilities/Constants.cs @@ -28,5 +28,10 @@ public class Constants /// error message on standard error. /// public const int StandardErrorMaxLength = 8192; // 8 KB + + /// + /// Environment Variable Specified by user to setup Culture. + /// + public const string DotNetUserSpecifiedCulture = "DOTNET_CLI_UI_LANGUAGE"; } } diff --git a/src/datacollector/DataCollectorMain.cs b/src/datacollector/DataCollectorMain.cs index 7e61df298b..106cbda3c5 100644 --- a/src/datacollector/DataCollectorMain.cs +++ b/src/datacollector/DataCollectorMain.cs @@ -90,6 +90,8 @@ public void Run(string[] args) EqtTrace.DoNotInitailize = true; } + SetCultureSpecifiedByUser(); + EqtTrace.Info("DataCollectorMain.Run: Starting data collector run with args: {0}", string.Join(",", args)); // Attach to exit of parent process @@ -140,6 +142,22 @@ private void WaitForDebuggerIfEnabled() } } + private static void SetCultureSpecifiedByUser() + { + var userCultureSpecified = Environment.GetEnvironmentVariable(CoreUtilities.Constants.DotNetUserSpecifiedCulture); + if (!string.IsNullOrWhiteSpace(userCultureSpecified)) + { + try + { + CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(userCultureSpecified); + } + catch (Exception) + { + EqtTrace.Info(string.Format("Invalid Culture Info: {0}", userCultureSpecified)); + } + } + } + private void StartProcessing() { var timeout = EnvironmentHelper.GetConnectionTimeout(); diff --git a/src/testhost.x86/AppDomainEngineInvoker.cs b/src/testhost.x86/AppDomainEngineInvoker.cs index 03a940783c..b59175a818 100644 --- a/src/testhost.x86/AppDomainEngineInvoker.cs +++ b/src/testhost.x86/AppDomainEngineInvoker.cs @@ -13,6 +13,7 @@ namespace Microsoft.VisualStudio.TestPlatform.TestHost using System.Reflection; using System.Xml.Linq; using System.Collections.Generic; + using System.Globalization; /// /// Implementation for the Invoker which invokes engine in a new AppDomain @@ -28,16 +29,31 @@ namespace Microsoft.VisualStudio.TestPlatform.TestHost private string mergedTempConfigFile = null; - public AppDomainEngineInvoker(string testSourcePath) + public AppDomainEngineInvoker(string testSourcePath, AppDomainInitializer initializer = null, string appBasePath = null) { TestPlatformEventSource.Instance.TestHostAppDomainCreationStart(); - this.appDomain = CreateNewAppDomain(testSourcePath); + this.appDomain = CreateNewAppDomain(testSourcePath, initializer, appBasePath); + + // Setting appbase later, as AppDomain needs to load testhost.exe into the new Domain, to have access to AppDomainInitializer method. + // If we set appbase to testsource folder, then if fails to find testhost.exe resulting in FileNotFoundException for testhost.exe + this.UpdateAppBaseToTestSourceLocation(testSourcePath); this.actualInvoker = CreateInvokerInAppDomain(appDomain); TestPlatformEventSource.Instance.TestHostAppDomainCreationStop(); } + private void UpdateAppBaseToTestSourceLocation(string testSourcePath) + { + // Set AppBase to TestAssembly location + var testSourceFolder = Path.GetDirectoryName(testSourcePath); + if (this.appDomain != null) + { + this.appDomain.SetData("APPBASE", testSourceFolder); + } + } + + /// /// Invokes the Engine with the arguments /// @@ -71,15 +87,22 @@ public void Invoke(IDictionary argsDictionary) } } - private AppDomain CreateNewAppDomain(string testSourcePath) + private AppDomain CreateNewAppDomain(string testSourcePath, AppDomainInitializer initializer, string appBasePath) { var appDomainSetup = new AppDomainSetup(); var testSourceFolder = Path.GetDirectoryName(testSourcePath); - // Set AppBase to TestAssembly location - appDomainSetup.ApplicationBase = testSourceFolder; + if (!string.IsNullOrEmpty(appBasePath)) + { + appDomainSetup.ApplicationBase = appBasePath; + } + appDomainSetup.LoaderOptimization = LoaderOptimization.MultiDomainHost; + //Setup AppDomainInitialzier to set user defined Culture + appDomainSetup.AppDomainInitializer = initializer ?? SetAppDomainCulture; + appDomainSetup.AppDomainInitializerArguments = new string[] { }; + // Set User Config file as app domain config SetConfigurationFile(appDomainSetup, testSourcePath, testSourceFolder); @@ -167,6 +190,23 @@ private static string GetConfigFile(string testSource, string testSourceFolder) return configFile; } + private static void SetAppDomainCulture(string[] args) + { + var userCultureSpecified = Environment.GetEnvironmentVariable(CoreUtilities.Constants.DotNetUserSpecifiedCulture); + if (!string.IsNullOrWhiteSpace(userCultureSpecified)) + { + try + { + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture(userCultureSpecified); + } + // If an exception occurs, we'll just fall back to the system default. + catch (Exception) + { + EqtTrace.Verbose("Invalid Culture Info '{0}:'", userCultureSpecified); + } + } + } + protected static XDocument MergeApplicationConfigFiles(XDocument userConfigDoc, XDocument testHostConfigDoc) { // Start with User's config file as the base diff --git a/src/testhost.x86/Program.cs b/src/testhost.x86/Program.cs index 48c7776429..674c4e0881 100644 --- a/src/testhost.x86/Program.cs +++ b/src/testhost.x86/Program.cs @@ -6,7 +6,7 @@ namespace Microsoft.VisualStudio.TestPlatform.TestHost using System; using System.Collections.Generic; using System.Diagnostics; - + using System.Globalization; using Microsoft.VisualStudio.TestPlatform.CoreUtilities.Helpers; using Microsoft.VisualStudio.TestPlatform.CoreUtilities.Tracing; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -52,6 +52,7 @@ public static void Main(string[] args) public static void Run(string[] args) { WaitForDebuggerIfEnabled(); + SetCultureSpecifiedByUser(); var argsDictionary = CommandLineArgumentsHelper.GetArgumentsDictionary(args); // Invoke the engine with arguments @@ -103,5 +104,21 @@ private static void WaitForDebuggerIfEnabled() Debugger.Break(); } } + + private static void SetCultureSpecifiedByUser() + { + var userCultureSpecified = Environment.GetEnvironmentVariable(CoreUtilities.Constants.DotNetUserSpecifiedCulture); + if (!string.IsNullOrWhiteSpace(userCultureSpecified)) + { + try + { + CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(userCultureSpecified); + } + catch (Exception) + { + ConsoleOutput.Instance.WriteLine(string.Format("Invalid Culture Info: {0}", userCultureSpecified), OutputLevel.Information); + } + } + } } } diff --git a/src/vstest.console/Program.cs b/src/vstest.console/Program.cs index 2ad37db30e..4363137a53 100644 --- a/src/vstest.console/Program.cs +++ b/src/vstest.console/Program.cs @@ -4,6 +4,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CommandLine { using System; + using System.Globalization; using Microsoft.VisualStudio.TestPlatform.Utilities; /// @@ -36,7 +37,25 @@ public static int Main(string[] args) System.Diagnostics.Debugger.Break(); } + SetCultureSpecifiedByUser(); + return new Executor(ConsoleOutput.Instance).Execute(args); } + + private static void SetCultureSpecifiedByUser() + { + var userCultureSpecified = Environment.GetEnvironmentVariable(CoreUtilities.Constants.DotNetUserSpecifiedCulture); + if(!string.IsNullOrWhiteSpace(userCultureSpecified)) + { + try + { + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture(userCultureSpecified); + } + catch(Exception) + { + ConsoleOutput.Instance.WriteLine(string.Format("Invalid Culture Info: {0}", userCultureSpecified), OutputLevel.Information); + } + } + } } } diff --git a/test/testhost.UnitTests/AppDomainEngineInvokerTests.cs b/test/testhost.UnitTests/AppDomainEngineInvokerTests.cs index 6632dee675..7706e4df9d 100644 --- a/test/testhost.UnitTests/AppDomainEngineInvokerTests.cs +++ b/test/testhost.UnitTests/AppDomainEngineInvokerTests.cs @@ -6,6 +6,7 @@ namespace testhost.UnitTests #if NET451 using Microsoft.VisualStudio.TestPlatform.TestHost; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.VisualStudio.TestPlatform.CoreUtilities; using System; using System.IO; using System.Collections.Generic; @@ -13,6 +14,7 @@ namespace testhost.UnitTests using System.Threading.Tasks; using System.Xml.Linq; using System.Text; + using System.Globalization; [TestClass] public class AppDomainEngineInvokerTests @@ -47,7 +49,7 @@ public class AppDomainEngineInvokerTests public void AppDomainEngineInvokerShouldCreateNewAppDomain() { var tempFile = Path.GetTempFileName(); - var appDomainInvoker = new TestableEngineInvoker(tempFile); + var appDomainInvoker = new TestableEngineInvoker(tempFile, AppDomain.CurrentDomain.BaseDirectory); Assert.IsNotNull(appDomainInvoker.NewAppDomain, "New AppDomain must be created."); Assert.IsNotNull(appDomainInvoker.ActualInvoker, "Invoker must be created."); @@ -55,11 +57,57 @@ public void AppDomainEngineInvokerShouldCreateNewAppDomain() "New AppDomain must be different from default one."); } + [TestMethod] + public void AppDomainEngineInvokerShouldCreateNewAppDomainAndSetCultureAsPerUsersInput() + { + var cultureInfo = "fr-FR"; + Environment.SetEnvironmentVariable(Constants.DotNetUserSpecifiedCulture, cultureInfo); + var tempFile = Path.GetTempFileName(); + var appDomainInvoker = new TestableEngineInvoker(tempFile, AppDomain.CurrentDomain.BaseDirectory); + appDomainInvoker.Invoke(new Dictionary()); + + Assert.IsNotNull(appDomainInvoker.NewAppDomain, "New AppDomain must be created."); + Assert.IsNotNull(appDomainInvoker.ActualInvoker, "Invoker must be created."); + + Assert.AreEqual((appDomainInvoker.ActualInvoker as MockEngineInvoker).CurrentCultureInfo, cultureInfo); + } + + [TestMethod] + public void AppDomainEngineInvokerShouldCreateNewAppDomainAndSetAppBaseToSourceDirectory() + { + var cultureInfo = "fr-FR"; + Environment.SetEnvironmentVariable(Constants.DotNetUserSpecifiedCulture, cultureInfo); + var tempFile = Path.GetTempFileName(); + var appDomainInvoker = new TestableEngineInvoker(tempFile, AppDomain.CurrentDomain.BaseDirectory); + + Assert.IsNotNull(appDomainInvoker.NewAppDomain, "New AppDomain must be created."); + Assert.IsNotNull(appDomainInvoker.ActualInvoker, "Invoker must be created."); + + Assert.AreEqual(appDomainInvoker.NewAppDomain.BaseDirectory, Path.GetDirectoryName(tempFile)); + } + + [TestMethod] + [DataRow("garbage-culture")] + [DataRow(" ")] + [DataRow(null)] + public void AppDomainEngineInvokerShouldCreateNewAppDomainAndSetCultureToEnglishIfUsersInputIncorrect(string cultureInfo) + { + Environment.SetEnvironmentVariable(Constants.DotNetUserSpecifiedCulture, cultureInfo); + var tempFile = Path.GetTempFileName(); + var appDomainInvoker = new TestableEngineInvoker(tempFile, AppDomain.CurrentDomain.BaseDirectory); + appDomainInvoker.Invoke(new Dictionary()); + + Assert.IsNotNull(appDomainInvoker.NewAppDomain, "New AppDomain must be created."); + Assert.IsNotNull(appDomainInvoker.ActualInvoker, "Invoker must be created."); + + Assert.AreEqual((appDomainInvoker.ActualInvoker as MockEngineInvoker).CurrentCultureInfo, "en-US"); + } + [TestMethod] public void AppDomainEngineInvokerShouldInvokeEngineInNewDomainAndUseTestHostConfigFile() { var tempFile = Path.GetTempFileName(); - var appDomainInvoker = new TestableEngineInvoker(tempFile); + var appDomainInvoker = new TestableEngineInvoker(tempFile, AppDomain.CurrentDomain.BaseDirectory); var newAppDomain = appDomainInvoker.NewAppDomain; @@ -200,10 +248,26 @@ public void AppDomainEngineInvokerShouldUseDiagAndAppSettingsElementsUnMergedFro private class TestableEngineInvoker : AppDomainEngineInvoker { - public TestableEngineInvoker(string testSourcePath) : base(testSourcePath) + public TestableEngineInvoker(string testSourcePath, string appBasePath) : base(testSourcePath, SetAppDomainCultures, appBasePath) { } + private static void SetAppDomainCultures(string[] args) + { + var userCultureSpecified = Environment.GetEnvironmentVariable(Constants.DotNetUserSpecifiedCulture); + if (!string.IsNullOrWhiteSpace(userCultureSpecified)) + { + try + { + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture(userCultureSpecified); + } + // If an exception occurs, we'll just fall back to the system default. + catch (Exception) + { + } + } + } + public static XDocument MergeConfigXmls(string userConfigText, string testHostConfigText) { return MergeApplicationConfigFiles( @@ -219,10 +283,11 @@ public static XDocument MergeConfigXmls(string userConfigText, string testHostCo private class MockEngineInvoker : MarshalByRefObject, IEngineInvoker { public string DomainFriendlyName { get; private set; } - + public string CurrentCultureInfo { get; set; } public void Invoke(IDictionary argsDictionary) { this.DomainFriendlyName = AppDomain.CurrentDomain.FriendlyName; + this.CurrentCultureInfo = CultureInfo.CurrentUICulture.Name; } } }