diff --git a/src/Platform/Microsoft.Testing.Platform/CommandLine/PlatformCommandLineProvider.cs b/src/Platform/Microsoft.Testing.Platform/CommandLine/PlatformCommandLineProvider.cs index d744fdd77b..33029f8f19 100644 --- a/src/Platform/Microsoft.Testing.Platform/CommandLine/PlatformCommandLineProvider.cs +++ b/src/Platform/Microsoft.Testing.Platform/CommandLine/PlatformCommandLineProvider.cs @@ -111,7 +111,7 @@ public Task ValidateOptionArgumentsAsync(CommandLineOption com { string arg = arguments[0]; int size = arg.Length; - if ((char.ToLowerInvariant(arg[size - 1]) != 'h' && char.ToLowerInvariant(arg[size - 1]) != 'm' && char.ToLowerInvariant(arg[size - 1]) != 's') || !float.TryParse(arg[..(size - 1)], out float _)) + if ((char.ToLowerInvariant(arg[size - 1]) != 'h' && char.ToLowerInvariant(arg[size - 1]) != 'm' && char.ToLowerInvariant(arg[size - 1]) != 's') || !float.TryParse(arg[..(size - 1)], NumberStyles.Float, CultureInfo.InvariantCulture, out float _)) { return ValidationResult.InvalidTask(PlatformResources.PlatformCommandLineTimeoutArgumentErrorMessage); } diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs index ac796585c4..bd38701961 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs @@ -255,7 +255,7 @@ public async Task BuildAsync( { string arg = args[0]; int size = arg.Length; - if (!float.TryParse(arg[..(size - 1)], out float value)) + if (!float.TryParse(arg[..(size - 1)], NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { throw ApplicationStateGuard.Unreachable(); } diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/CommandLine/PlatformCommandLineProviderTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/CommandLine/PlatformCommandLineProviderTests.cs index 4ea02fad22..f838ea4e3c 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/CommandLine/PlatformCommandLineProviderTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/CommandLine/PlatformCommandLineProviderTests.cs @@ -181,4 +181,77 @@ public async Task IsNotValid_If_ExitOnProcess_Not_Running() Assert.IsFalse(validateOptionsResult.IsValid); Assert.IsTrue(validateOptionsResult.ErrorMessage.StartsWith($"Invalid PID '{pid}'", StringComparison.OrdinalIgnoreCase)); } + + [TestMethod] + [DataRow("1.5s")] + [DataRow("2.0m")] + [DataRow("0.5h")] + [DataRow("10s")] + [DataRow("30m")] + [DataRow("1h")] + public async Task IsValid_If_Timeout_Has_CorrectFormat_InvariantCulture(string timeout) + { + var provider = new PlatformCommandLineProvider(); + CommandLineOption option = provider.GetCommandLineOptions().First(x => x.Name == PlatformCommandLineProvider.TimeoutOptionKey); + + // Save current culture + CultureInfo originalCulture = CultureInfo.CurrentCulture; + try + { + // Test with various cultures to ensure invariant parsing works + foreach (string cultureName in new[] { "en-US", "de-DE", "fr-FR" }) + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(cultureName); + ValidationResult validateOptionsResult = await provider.ValidateOptionArgumentsAsync(option, [timeout]); + Assert.IsTrue(validateOptionsResult.IsValid, $"Failed with culture {cultureName} and timeout {timeout}"); + Assert.IsTrue(string.IsNullOrEmpty(validateOptionsResult.ErrorMessage)); + } + } + finally + { + // Restore original culture + CultureInfo.CurrentCulture = originalCulture; + } + } + + [TestMethod] + [DataRow("1,5s")] // German decimal separator + [DataRow("invalid")] + [DataRow("1.5")] // Missing unit + [DataRow("abc.5s")] + public async Task IsInvalid_If_Timeout_Has_IncorrectFormat(string timeout) + { + var provider = new PlatformCommandLineProvider(); + CommandLineOption option = provider.GetCommandLineOptions().First(x => x.Name == PlatformCommandLineProvider.TimeoutOptionKey); + + ValidationResult validateOptionsResult = await provider.ValidateOptionArgumentsAsync(option, [timeout]); + Assert.IsFalse(validateOptionsResult.IsValid); + Assert.AreEqual(PlatformResources.PlatformCommandLineTimeoutArgumentErrorMessage, validateOptionsResult.ErrorMessage); + } + + [TestMethod] + public async Task Timeout_Parsing_Uses_InvariantCulture_NotCurrentCulture() + { + var provider = new PlatformCommandLineProvider(); + CommandLineOption option = provider.GetCommandLineOptions().First(x => x.Name == PlatformCommandLineProvider.TimeoutOptionKey); + + // Save current culture + CultureInfo originalCulture = CultureInfo.CurrentCulture; + try + { + // Set culture to German where decimal separator is comma + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de-DE"); + // This should work because we use invariant culture (period as decimal separator) + ValidationResult validResult = await provider.ValidateOptionArgumentsAsync(option, ["1.5s"]); + Assert.IsTrue(validResult.IsValid, "1.5s should be valid when using invariant culture"); + // This should fail because comma is not valid in invariant culture + ValidationResult invalidResult = await provider.ValidateOptionArgumentsAsync(option, ["1,5s"]); + Assert.IsFalse(invalidResult.IsValid, "1,5s should be invalid when using invariant culture"); + } + finally + { + // Restore original culture + CultureInfo.CurrentCulture = originalCulture; + } + } }