diff --git a/src/xunit.runner.visualstudio/Utility/RunSettings.cs b/src/xunit.runner.visualstudio/Utility/RunSettings.cs index 5cdf3ff..611de93 100644 --- a/src/xunit.runner.visualstudio/Utility/RunSettings.cs +++ b/src/xunit.runner.visualstudio/Utility/RunSettings.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text.RegularExpressions; namespace Xunit.Runner.VisualStudio; @@ -94,8 +95,28 @@ public static RunSettings Parse(string? settingsXml) result.LongRunningTestSeconds = longRunningTestSeconds; var maxParallelThreadsString = xunitElement.Element(Constants.Xunit.MaxParallelThreads)?.Value; - if (int.TryParse(maxParallelThreadsString, out var maxParallelThreads) && maxParallelThreads >= -1) - result.MaxParallelThreads = maxParallelThreads; + if (maxParallelThreadsString is not null) + switch (maxParallelThreadsString) + { + case "default": + result.MaxParallelThreads = 0; + break; + + case "unlimited": + result.MaxParallelThreads = -1; + break; + + default: + var match = ConfigUtility.MultiplierStyleMaxParallelThreadsRegex.Match(maxParallelThreadsString); + // Use invariant format and convert ',' to '.' so we can always support both formats, regardless of locale + // If we stick to locale-only parsing, we could break people when moving from one locale to another (for example, + // from people running tests on their desktop in a comma locale vs. running them in CI with a decimal locale). + if (match.Success && decimal.TryParse(match.Groups[1].Value.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var maxThreadMultiplier)) + result.MaxParallelThreads = (int)(maxThreadMultiplier * Environment.ProcessorCount); + else if (int.TryParse(maxParallelThreadsString, out var threadValue) && threadValue >= -1) + result.MaxParallelThreads = threadValue; + break; + } var methodDisplayString = xunitElement.Element(Constants.Xunit.MethodDisplay)?.Value; if (Enum.TryParse(methodDisplayString, ignoreCase: true, out var methodDisplay)) diff --git a/test/test.xunit.runner.visualstudio/RunSettingsTests.cs b/test/test.xunit.runner.visualstudio/RunSettingsTests.cs index 255c8ea..60126e0 100644 --- a/test/test.xunit.runner.visualstudio/RunSettingsTests.cs +++ b/test/test.xunit.runner.visualstudio/RunSettingsTests.cs @@ -1,3 +1,4 @@ +using System; using Xunit; using Xunit.Runner.VisualStudio; @@ -16,6 +17,7 @@ void AssertDefaultValues(RunSettings runSettings) Assert.Null(runSettings.MethodDisplay); Assert.Null(runSettings.MethodDisplayOptions); Assert.Null(runSettings.NoAutoReporters); + Assert.Null(runSettings.ParallelAlgorithm); Assert.Null(runSettings.ParallelizeAssembly); Assert.Null(runSettings.ParallelizeTestCollections); Assert.Null(runSettings.PreEnumerateTheories); @@ -182,11 +184,21 @@ public void LongRunningTestSeconds(int value, int? expected) Assert.Equal(expected, runSettings.LongRunningTestSeconds); } + public static readonly TheoryData MaxParallelThreadData = new() + { + { "blarch", null }, + { "-2", null }, + { "-1", -1 }, + { "0", 0 }, + { "42", 42 }, + { "unlimited", -1 }, + { "default", 0 }, + { "2.0x", Environment.ProcessorCount * 2 }, + }; + [Theory] - [InlineData(-2, null)] - [InlineData(-1, -1)] - [InlineData(42, 42)] - public void MaxParallelThreads(int value, int? expected) + [MemberData(nameof(MaxParallelThreadData))] + public void MaxParallelThreads(string value, int? expected) { string settingsXml = $@"