diff --git a/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs b/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs index 9a694bcdf9..dab7b1550a 100644 --- a/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs +++ b/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs @@ -43,6 +43,7 @@ internal TestMethodInfo( TestMethod = testMethod; Parent = parent; TestMethodOptions = testMethodOptions; + TestCultureName = GetAttributes(inherit: false).FirstOrDefault()?.CultureName; } /// @@ -87,6 +88,8 @@ internal TestMethodInfo( /// internal TestMethodOptions TestMethodOptions { get; } + internal string? TestCultureName { get; } + public Attribute[]? GetAllAttributes(bool inherit) => ReflectHelper.Instance.GetDerivedAttributes(TestMethod, inherit).ToArray(); public TAttributeType[] GetAttributes(bool inherit) diff --git a/src/TestFramework/TestFramework.Extensions/Attributes/UWP_UITestMethodAttribute.cs b/src/TestFramework/TestFramework.Extensions/Attributes/UWP_UITestMethodAttribute.cs index c4bc2f1b17..495bd82daf 100644 --- a/src/TestFramework/TestFramework.Extensions/Attributes/UWP_UITestMethodAttribute.cs +++ b/src/TestFramework/TestFramework.Extensions/Attributes/UWP_UITestMethodAttribute.cs @@ -33,7 +33,13 @@ public override TestResult[] Execute(ITestMethod testMethod) TestResult? result = null; Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( Windows.UI.Core.CoreDispatcherPriority.Normal, - () => result = testMethod.Invoke(null)).AsTask().GetAwaiter().GetResult(); + () => + { + using (SetCultureForTest(testMethod)) + { + result = testMethod.Invoke(null); + } + }).AsTask().GetAwaiter().GetResult(); return [result!]; } diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/TestCultureAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/TestCultureAttribute.cs new file mode 100644 index 0000000000..102a44f20d --- /dev/null +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/TestCultureAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Attribute to specify the CultureInfo.CurrentCulture and CultureInfo.CurrentUICulture when running the test. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public sealed class TestCultureAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The culture to be used for the test. For example, "en-US". + /// + public TestCultureAttribute(string cultureName) => CultureName = cultureName; + + /// + /// Gets the owner. + /// + public string CultureName { get; } +} diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/TestMethodAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/TestMethodAttribute.cs index bca969e00f..79fac62559 100644 --- a/src/TestFramework/TestFramework/Attributes/TestMethod/TestMethodAttribute.cs +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/TestMethodAttribute.cs @@ -1,6 +1,8 @@ // 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; + namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// @@ -9,6 +11,29 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; [AttributeUsage(AttributeTargets.Method)] public class TestMethodAttribute : Attribute { + private protected readonly struct ScopedCultureDisposable : IDisposable + { + private readonly CultureInfo _previousCulture; + private readonly CultureInfo _previousUICulture; + + public ScopedCultureDisposable(string cultureName) + { + _previousCulture = CultureInfo.CurrentCulture; + _previousUICulture = CultureInfo.CurrentUICulture; + + var newCulture = new CultureInfo(cultureName); + // TODO: Should we set both? Should we have different attribute? Same attribute with two arguments? + CultureInfo.CurrentCulture = newCulture; + CultureInfo.CurrentUICulture = newCulture; + } + + public void Dispose() + { + CultureInfo.CurrentCulture = _previousCulture; + CultureInfo.CurrentUICulture = _previousUICulture; + } + } + /// /// Initializes a new instance of the class. /// @@ -36,5 +61,19 @@ public TestMethodAttribute() /// The test method to execute. /// An array of TestResult objects that represent the outcome(s) of the test. /// Extensions can override this method to customize running a TestMethod. - public virtual TestResult[] Execute(ITestMethod testMethod) => [testMethod.Invoke(null)]; + public virtual TestResult[] Execute(ITestMethod testMethod) + { + using (SetCultureForTest(testMethod)) + { + return [testMethod.Invoke(null)]; + } + } + + // TestMethodInfo isn't accessible here :/ + // Can we add TestCultureName to the *public* interface? + // Or should we introduce an internal interface ITestMethod2 : ITestMethod :/ + private protected static ScopedCultureDisposable? SetCultureForTest(ITestMethod testMethod) + => testMethod is TestMethodInfo testMethodInfo && testMethodInfo.TestCultureName is { } culture + ? new ScopedCultureDisposable(culture) + : (ScopedCultureDisposable?)null; } diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index ab058de62d..6716a23ff8 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.VisualStudio.TestTools.UnitTesting.TestCultureAttribute +Microsoft.VisualStudio.TestTools.UnitTesting.TestCultureAttribute.CultureName.get -> string! +Microsoft.VisualStudio.TestTools.UnitTesting.TestCultureAttribute.TestCultureAttribute(string! cultureName) -> void