diff --git a/src/Versions.props b/src/Versions.props index 9731ea39f..9fac4013d 100644 --- a/src/Versions.props +++ b/src/Versions.props @@ -37,7 +37,7 @@ 4.5.5 2.0.3 1.9.2 - 2.7.2-pre.2 + 2.7.2-pre.21 diff --git a/src/common.tests/TestDoubles/TestData.cs b/src/common.tests/TestDoubles/TestData.cs index 193c56faa..e173fa657 100644 --- a/src/common.tests/TestDoubles/TestData.cs +++ b/src/common.tests/TestDoubles/TestData.cs @@ -295,6 +295,7 @@ public static class TestData ExplicitOption? explicitOption = null, bool internalDiagnosticMessages = false, int maxParallelThreads = 2600, + ParallelAlgorithm? parallelAlgorithm = null, bool? parallelizeTestCollections = null, bool? stopOnFail = null, int? seed = null, @@ -315,6 +316,7 @@ public static class TestData ExplicitOption = explicitOption, InternalDiagnosticMessages = internalDiagnosticMessages, MaxParallelThreads = maxParallelThreads, + ParallelAlgorithm = parallelAlgorithm, ParallelizeTestCollections = parallelizeTestCollections, StopOnFail = stopOnFail, }); diff --git a/src/xunit.v2.tests/Sdk/Frameworks/Runners/XunitTestAssemblyRunnerTests.cs b/src/xunit.v2.tests/Sdk/Frameworks/Runners/XunitTestAssemblyRunnerTests.cs index c77fbb0d9..e2ae384f8 100644 --- a/src/xunit.v2.tests/Sdk/Frameworks/Runners/XunitTestAssemblyRunnerTests.cs +++ b/src/xunit.v2.tests/Sdk/Frameworks/Runners/XunitTestAssemblyRunnerTests.cs @@ -7,6 +7,7 @@ using Xunit; using Xunit.Abstractions; using Xunit.Sdk; +using ParallelAlgorithm = Xunit.ParallelAlgorithm; public class XunitTestAssemblyRunnerTests { @@ -165,12 +166,13 @@ public static void TestOptionsOverrideAttribute() public class RunAsync { [Fact] - public static async void Parallel_SingleThread() + public static async void Parallel_SingleThread_Aggressive() { var passing = Mocks.XunitTestCase("Passing"); var other = Mocks.XunitTestCase("Other"); var options = TestFrameworkOptions.ForExecution(); options.SetMaxParallelThreads(1); + options.SetParallelAlgorithm(ParallelAlgorithm.Aggressive); var runner = TestableXunitTestAssemblyRunner.Create(testCases: new[] { passing, other }, executionOptions: options); await runner.RunAsync(); diff --git a/src/xunit.v3.common/Internal/TestOptionsNames.cs b/src/xunit.v3.common/Internal/TestOptionsNames.cs index 17d741c40..c7de235e3 100644 --- a/src/xunit.v3.common/Internal/TestOptionsNames.cs +++ b/src/xunit.v3.common/Internal/TestOptionsNames.cs @@ -44,6 +44,8 @@ public static class Execution /// public static readonly string MaxParallelThreads = "xunit.execution.MaxParallelThreads"; /// + public static readonly string ParallelAlgorithm = "xunit.execution.ParallelAlgorithm"; + /// public static readonly string Seed = "xunit.execution.Seed"; /// public static readonly string SynchronousMessageReporting = "xunit.execution.SynchronousMessageReporting"; diff --git a/src/xunit.v3.common/Options/ParallelAlgorithm.cs b/src/xunit.v3.common/Options/ParallelAlgorithm.cs new file mode 100644 index 000000000..7013f0510 --- /dev/null +++ b/src/xunit.v3.common/Options/ParallelAlgorithm.cs @@ -0,0 +1,24 @@ +namespace Xunit.Sdk; + +/// +/// Indicates the parallelization algorithm to use. +/// +public enum ParallelAlgorithm +{ + /// + /// The conservative parallelization algorithm uses a semaphore to limit the number of started tests to be equal + /// to the desired parallel thread count. This has the effect of allowing tests that have started to finish faster, + /// since there are no extra tests competing for a chance to run, at the expense that CPU utilization will be lowered + /// if the test project spaws a lot of async tests that have significant wait times. + /// + Conservative = 0, + + /// + /// The aggressive parallelization algorithm uses a synchronization context to limit the number of running tests + /// to be equal to the desired parallel thread count. This has the effect of being able to use the CPU more + /// effectively since there are typically most tests capable of running than there are CPU cores, at the + /// expense of tests that have already started being put into the back of a long queue before they can run + /// again. + /// + Aggressive = 1, +} diff --git a/src/xunit.v3.core.tests/Sdk/v3/Runners/XunitTestAssemblyRunnerContextTests.cs b/src/xunit.v3.core.tests/Sdk/v3/Runners/XunitTestAssemblyRunnerContextTests.cs index 6f32183a9..1ddf7ab56 100644 --- a/src/xunit.v3.core.tests/Sdk/v3/Runners/XunitTestAssemblyRunnerContextTests.cs +++ b/src/xunit.v3.core.tests/Sdk/v3/Runners/XunitTestAssemblyRunnerContextTests.cs @@ -51,17 +51,24 @@ public static async ValueTask Attribute_NonParallel() Assert.EndsWith("[collection-per-class, non-parallel]", result); } - [Fact] - public static async ValueTask Attribute_MaxThreads() + [Theory] + [InlineData(1, null, "1 thread")] + [InlineData(3, ParallelAlgorithm.Conservative, "3 threads")] + [InlineData(42, ParallelAlgorithm.Aggressive, "42 threads/aggressive")] + public static async ValueTask Attribute_MaxThreads( + int maxThreads, + ParallelAlgorithm? parallelAlgorithm, + string expected) { - var attribute = Mocks.CollectionBehaviorAttribute(maxParallelThreads: 3); + var attribute = Mocks.CollectionBehaviorAttribute(maxParallelThreads: maxThreads); var assembly = Mocks.TestAssembly("assembly.dll", assemblyAttributes: new[] { attribute }); - await using var context = TestableXunitTestAssemblyRunnerContext.Create(assembly: assembly); + var options = _TestFrameworkOptions.ForExecution(parallelAlgorithm: parallelAlgorithm); + await using var context = TestableXunitTestAssemblyRunnerContext.Create(assembly: assembly, executionOptions: options); await context.InitializeAsync(); var result = context.TestFrameworkEnvironment; - Assert.EndsWith("[collection-per-class, parallel (3 threads)]", result); + Assert.EndsWith($"[collection-per-class, parallel ({expected})]", result); } [Fact] @@ -69,7 +76,8 @@ public static async ValueTask Attribute_Unlimited() { var attribute = Mocks.CollectionBehaviorAttribute(maxParallelThreads: -1); var assembly = Mocks.TestAssembly("assembly.dll", assemblyAttributes: new[] { attribute }); - await using var context = TestableXunitTestAssemblyRunnerContext.Create(assembly: assembly); + var options = _TestFrameworkOptions.ForExecution(parallelAlgorithm: ParallelAlgorithm.Aggressive); // Shouldn't show for unlimited threads + await using var context = TestableXunitTestAssemblyRunnerContext.Create(assembly: assembly, executionOptions: options); await context.InitializeAsync(); var result = context.TestFrameworkEnvironment; diff --git a/src/xunit.v3.core.tests/Sdk/v3/Runners/XunitTestAssemblyRunnerTests.cs b/src/xunit.v3.core.tests/Sdk/v3/Runners/XunitTestAssemblyRunnerTests.cs index 2ee2ef960..22cae120f 100644 --- a/src/xunit.v3.core.tests/Sdk/v3/Runners/XunitTestAssemblyRunnerTests.cs +++ b/src/xunit.v3.core.tests/Sdk/v3/Runners/XunitTestAssemblyRunnerTests.cs @@ -13,13 +13,17 @@ public class XunitTestAssemblyRunnerTests { public class RunAsync { + // This test is forced to use the aggressive algorithm so that we know we're running in a thread pool with + // a single thread. The default conserative algorithm runs in the .NET Thread Pool, so our async continuation + // could end up on any thread, despite the fact that are limited to running one test at a time. [Fact] - public static async ValueTask Parallel_SingleThread() + public static async ValueTask Parallel_SingleThread_Aggressive() { var passing = TestData.XunitTestCase("Passing"); var other = TestData.XunitTestCase("Other"); var options = _TestFrameworkOptions.ForExecution(); options.SetMaxParallelThreads(1); + options.SetParallelAlgorithm(ParallelAlgorithm.Aggressive); var runner = TestableXunitTestAssemblyRunner.Create(testCases: new[] { passing, other }, executionOptions: options); await runner.RunAsync(); diff --git a/src/xunit.v3.core/Extensions/TestFrameworkOptionsReadExtensions.cs b/src/xunit.v3.core/Extensions/TestFrameworkOptionsReadExtensions.cs index 2e6ca4d4e..7b9ce6f7b 100644 --- a/src/xunit.v3.core/Extensions/TestFrameworkOptionsReadExtensions.cs +++ b/src/xunit.v3.core/Extensions/TestFrameworkOptionsReadExtensions.cs @@ -239,6 +239,24 @@ public static int MaxParallelThreadsOrDefault(this _ITestFrameworkExecutionOptio return result.GetValueOrDefault(); } + /// + /// Gets the parallel algorithm to be used. + /// + public static ParallelAlgorithm? ParallelAlgorithm(this _ITestFrameworkExecutionOptions executionOptions) + { + Guard.ArgumentNotNull(executionOptions); + + var parallelAlgorithmString = executionOptions.GetValue(TestOptionsNames.Execution.ParallelAlgorithm); + return parallelAlgorithmString != null ? (ParallelAlgorithm?)Enum.Parse(typeof(ParallelAlgorithm), parallelAlgorithmString) : null; + } + + /// + /// Gets the parallel algorithm to be used. If the flag is not present, return the default + /// value (). + /// + public static ParallelAlgorithm ParallelAlgorithmOrDefault(this _ITestFrameworkExecutionOptions executionOptions) => + ParallelAlgorithm(executionOptions) ?? Sdk.ParallelAlgorithm.Conservative; + /// /// Gets the value that should be used to seed randomness. /// diff --git a/src/xunit.v3.core/Sdk/v3/Runners/XunitTestAssemblyRunner.cs b/src/xunit.v3.core/Sdk/v3/Runners/XunitTestAssemblyRunner.cs index e5eef4f86..b754b3e65 100644 --- a/src/xunit.v3.core/Sdk/v3/Runners/XunitTestAssemblyRunner.cs +++ b/src/xunit.v3.core/Sdk/v3/Runners/XunitTestAssemblyRunner.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Xunit.Internal; @@ -54,7 +53,6 @@ protected override async ValueTask BeforeTestAssemblyFinishedAsync(XunitTestAsse await base.BeforeTestAssemblyFinishedAsync(ctxt); } - /// protected override ITestCaseOrderer GetTestCaseOrderer(XunitTestAssemblyRunnerContext ctxt) => Guard.ArgumentNotNull(ctxt).AssemblyTestCaseOrderer ?? base.GetTestCaseOrderer(ctxt); @@ -97,7 +95,7 @@ protected override async ValueTask RunTestCollectionsAsync(XunitTest if (ctxt.DisableParallelization) return await base.RunTestCollectionsAsync(ctxt); - ctxt.SetupMaxConcurrencySyncContext(); + ctxt.SetupParallelism(); Func>, ValueTask> taskRunner; if (SynchronizationContext.Current is not null) @@ -162,15 +160,6 @@ protected override async ValueTask RunTestCollectionsAsync(XunitTest Guard.ArgumentNotNull(testCollection); Guard.ArgumentNotNull(testCases); - return XunitTestCollectionRunner.Instance.RunAsync( - testCollection, - testCases, - ctxt.ExplicitOption, - ctxt.MessageBus, - GetTestCaseOrderer(ctxt), - ctxt.Aggregator.Clone(), - ctxt.CancellationTokenSource, - ctxt.AssemblyFixtureMappings - ); + return ctxt.RunTestCollectionAsync(testCollection, testCases, GetTestCaseOrderer(ctxt)); } } diff --git a/src/xunit.v3.core/Sdk/v3/Runners/XunitTestAssemblyRunnerContext.cs b/src/xunit.v3.core/Sdk/v3/Runners/XunitTestAssemblyRunnerContext.cs index 46b666a78..7e9bf4cc5 100644 --- a/src/xunit.v3.core/Sdk/v3/Runners/XunitTestAssemblyRunnerContext.cs +++ b/src/xunit.v3.core/Sdk/v3/Runners/XunitTestAssemblyRunnerContext.cs @@ -16,6 +16,7 @@ namespace Xunit.v3; public class XunitTestAssemblyRunnerContext : TestAssemblyRunnerContext { _IAttributeInfo? collectionBehaviorAttribute; + SemaphoreSlim? parallelSemaphore; MaxConcurrencySyncContext? syncContext; /// @@ -56,6 +57,11 @@ public class XunitTestAssemblyRunnerContext : TestAssemblyRunnerContext public int MaxParallelThreads { get; private set; } + /// + /// Gets the algorithm used for parallelism. + /// + public ParallelAlgorithm ParallelAlgorithm { get; private set; } + /// public override string TestFrameworkDisplayName => XunitTestFrameworkDiscoverer.DisplayName; @@ -69,6 +75,13 @@ public override string TestFrameworkEnvironment ExtensibilityPointFactory.GetXunitTestCollectionFactory(collectionBehaviorAttribute, TestAssembly) ?? new CollectionPerClassTestCollectionFactory(TestAssembly); + var threadCountText = MaxParallelThreads < 0 ? "unlimited" : MaxParallelThreads.ToString(CultureInfo.CurrentCulture); + threadCountText += " thread"; + if (MaxParallelThreads != 1) + threadCountText += 's'; + if (MaxParallelThreads > 0 && ParallelAlgorithm == ParallelAlgorithm.Aggressive) + threadCountText += "/aggressive"; + return string.Format( CultureInfo.CurrentCulture, "{0} [{1}, {2}]", @@ -76,11 +89,7 @@ public override string TestFrameworkEnvironment testCollectionFactory.DisplayName, DisableParallelization ? "non-parallel" - : string.Format( - CultureInfo.CurrentCulture, - "parallel ({0} threads)", - MaxParallelThreads < 0 ? "unlimited" : MaxParallelThreads.ToString(CultureInfo.CurrentCulture) - ) + : string.Format(CultureInfo.CurrentCulture, "parallel ({0})", threadCountText) ); } } @@ -95,6 +104,8 @@ public override async ValueTask DisposeAsync() else if (syncContext is IDisposable disposable) disposable.Dispose(); + parallelSemaphore?.Dispose(); + await base.DisposeAsync(); } @@ -110,6 +121,7 @@ public override async ValueTask InitializeAsync() MaxParallelThreads = collectionBehaviorAttribute.GetNamedArgument(nameof(CollectionBehaviorAttribute.MaxParallelThreads)); } + ParallelAlgorithm = ExecutionOptions.ParallelAlgorithm() ?? ParallelAlgorithm; DisableParallelization = ExecutionOptions.DisableParallelization() ?? DisableParallelization; MaxParallelThreads = ExecutionOptions.MaxParallelThreads() ?? MaxParallelThreads; if (MaxParallelThreads == 0) @@ -175,15 +187,61 @@ public override async ValueTask InitializeAsync() } /// - /// Sets up the sync context needed for limiting maximum concurrency, if so configured. + /// Delegation of that properly obeys the parallel + /// algorithm requirements. /// - public virtual void SetupMaxConcurrencySyncContext() + public async ValueTask RunTestCollectionAsync( + _ITestCollection testCollection, + IReadOnlyCollection testCases, + ITestCaseOrderer testCaseOrderer) { - if (MaxConcurrencySyncContext.IsSupported && MaxParallelThreads > 0) + if (parallelSemaphore is not null) + await parallelSemaphore.WaitAsync(CancellationTokenSource.Token); + + try + { + return await XunitTestCollectionRunner.Instance.RunAsync( + testCollection, + testCases, + ExplicitOption, + MessageBus, + testCaseOrderer, + Aggregator.Clone(), + CancellationTokenSource, + AssemblyFixtureMappings + ); + } + finally + { + parallelSemaphore?.Release(); + } + } + + /// + /// Sets up the mechanics for parallelism. + /// + public virtual void SetupParallelism() + { + // When unlimited, we just launch everything and let the .NET Thread Pool sort it out + if (MaxParallelThreads < 0) + return; + + // For aggressive, we launch everything and let our sync context limit what's allowed to run + if (ParallelAlgorithm == ParallelAlgorithm.Aggressive && MaxConcurrencySyncContext.IsSupported) { syncContext = new MaxConcurrencySyncContext(MaxParallelThreads); SetupSyncContextInternal(syncContext); } + // For conversative, we use a semaphore to limit the number of launched tests, and ensure + // that the .NET Thread Pool has enough threads based on the user's requested maximum + else + { + parallelSemaphore = new(initialCount: MaxParallelThreads); + + ThreadPool.GetMinThreads(out var minThreads, out var minIOPorts); + if (minThreads < MaxParallelThreads) + ThreadPool.SetMinThreads(MaxParallelThreads, minIOPorts); + } } [SecuritySafeCritical] diff --git a/src/xunit.v3.runner.common.tests/Reporters/DefaultRunnerReporterMessageHandlerTests.cs b/src/xunit.v3.runner.common.tests/Reporters/DefaultRunnerReporterMessageHandlerTests.cs index 88681a7af..be21f9c00 100644 --- a/src/xunit.v3.runner.common.tests/Reporters/DefaultRunnerReporterMessageHandlerTests.cs +++ b/src/xunit.v3.runner.common.tests/Reporters/DefaultRunnerReporterMessageHandlerTests.cs @@ -298,15 +298,17 @@ public static void LogsMessage() public class OnMessage_TestAssemblyExecutionStarting { [Theory] - [InlineData(false, null, null, null, null, null, "[Imp] => Starting: test-assembly")] - [InlineData(true, false, null, null, null, null, "[Imp] => Starting: test-assembly (parallel test collections = off, stop on fail = off, explicit = only)")] - [InlineData(true, null, -1, null, null, null, "[Imp] => Starting: test-assembly (parallel test collections = on [unlimited threads], stop on fail = off, explicit = only)")] - [InlineData(true, null, 1, null, null, null, "[Imp] => Starting: test-assembly (parallel test collections = on [1 thread], stop on fail = off, explicit = only)")] - [InlineData(true, null, null, true, null, null, "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = on, explicit = only)")] - [InlineData(true, null, null, null, null, null, "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = off, explicit = only)")] - [InlineData(true, null, null, null, 2112, null, "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = off, explicit = only, seed = 2112)")] - [InlineData(true, null, null, null, null, "", "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = off, explicit = only, culture = invariant)")] - [InlineData(true, null, null, null, null, "en-US", "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = off, explicit = only, culture = en-US)")] + [InlineData(false, null, null, null, null, null, null, "[Imp] => Starting: test-assembly")] + [InlineData(true, false, null, null, null, null, ParallelAlgorithm.Aggressive, "[Imp] => Starting: test-assembly (parallel test collections = off, stop on fail = off, explicit = only)")] + [InlineData(true, null, -1, null, null, null, ParallelAlgorithm.Conservative, "[Imp] => Starting: test-assembly (parallel test collections = on [unlimited threads], stop on fail = off, explicit = only)")] + [InlineData(true, null, -1, null, null, null, ParallelAlgorithm.Aggressive, "[Imp] => Starting: test-assembly (parallel test collections = on [unlimited threads], stop on fail = off, explicit = only)")] + [InlineData(true, null, 1, null, null, null, ParallelAlgorithm.Conservative, "[Imp] => Starting: test-assembly (parallel test collections = on [1 thread], stop on fail = off, explicit = only)")] + [InlineData(true, null, 1, null, null, null, ParallelAlgorithm.Aggressive, "[Imp] => Starting: test-assembly (parallel test collections = on [1 thread/aggressive], stop on fail = off, explicit = only)")] + [InlineData(true, null, null, true, null, null, null, "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = on, explicit = only)")] + [InlineData(true, null, null, null, null, null, null, "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = off, explicit = only)")] + [InlineData(true, null, null, null, 2112, null, null, "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = off, explicit = only, seed = 2112)")] + [InlineData(true, null, null, null, null, "", null, "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = off, explicit = only, culture = invariant)")] + [InlineData(true, null, null, null, null, "en-US", null, "[Imp] => Starting: test-assembly (parallel test collections = on [42 threads], stop on fail = off, explicit = only, culture = en-US)")] public static void LogsMessage( bool diagnosticMessages, bool? parallelizeTestCollections, @@ -314,6 +316,7 @@ public class OnMessage_TestAssemblyExecutionStarting bool? stopOnFail, int? seed, string? culture, + ParallelAlgorithm? parallelAlgorithm, string expectedResult) { var message = TestData.TestAssemblyExecutionStarting( @@ -322,6 +325,7 @@ public class OnMessage_TestAssemblyExecutionStarting maxParallelThreads: maxThreads ?? 42, stopOnFail: stopOnFail, explicitOption: ExplicitOption.Only, + parallelAlgorithm: parallelAlgorithm, seed: seed, culture: culture ); diff --git a/src/xunit.v3.runner.common/Configuration/ConfigReader_Json.cs b/src/xunit.v3.runner.common/Configuration/ConfigReader_Json.cs index e792da74b..6734ceea1 100644 --- a/src/xunit.v3.runner.common/Configuration/ConfigReader_Json.cs +++ b/src/xunit.v3.runner.common/Configuration/ConfigReader_Json.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text.Json; using Xunit.Internal; +using Xunit.Sdk; using Xunit.v3; namespace Xunit.Runner.Common; @@ -163,6 +164,11 @@ public static class ConfigReader_Json else configuration.Culture = stringValue; } + else if (string.Equals(property.Name, Configuration.ParallelAlgorithm, StringComparison.OrdinalIgnoreCase)) + { + if (Enum.TryParse(stringValue, true, out var parallelAlgorithm)) + configuration.ParallelAlgorithm = parallelAlgorithm; + } } } } @@ -189,6 +195,7 @@ static class Configuration public const string MaxParallelThreads = "maxParallelThreads"; public const string MethodDisplay = "methodDisplay"; public const string MethodDisplayOptions = "methodDisplayOptions"; + public const string ParallelAlgorithm = "parallelAlgorithm"; public const string ParallelizeAssembly = "parallelizeAssembly"; public const string ParallelizeTestCollections = "parallelizeTestCollections"; public const string PreEnumerateTheories = "preEnumerateTheories"; diff --git a/src/xunit.v3.runner.common/Extensions/TestFrameworkOptionsReadWriteExtensions.cs b/src/xunit.v3.runner.common/Extensions/TestFrameworkOptionsReadWriteExtensions.cs index a53566d75..4b0666bd7 100644 --- a/src/xunit.v3.runner.common/Extensions/TestFrameworkOptionsReadWriteExtensions.cs +++ b/src/xunit.v3.runner.common/Extensions/TestFrameworkOptionsReadWriteExtensions.cs @@ -381,6 +381,24 @@ public static int GetMaxParallelThreadsOrDefault(this _ITestFrameworkExecutionOp return result.GetValueOrDefault(); } + /// + /// Gets the parallel algorithm to be used. + /// + public static ParallelAlgorithm? GetParallelAlgorithm(this _ITestFrameworkExecutionOptions executionOptions) + { + Guard.ArgumentNotNull(executionOptions); + + var parallelAlgorithmString = executionOptions.GetValue(TestOptionsNames.Execution.ParallelAlgorithm); + return parallelAlgorithmString != null ? (ParallelAlgorithm?)Enum.Parse(typeof(ParallelAlgorithm), parallelAlgorithmString) : null; + } + + /// + /// Gets the parallel algorithm to be used. If the flag is not present, return the default + /// value (). + /// + public static ParallelAlgorithm GetParallelAlgorithmOrDefault(this _ITestFrameworkExecutionOptions executionOptions) => + GetParallelAlgorithm(executionOptions) ?? ParallelAlgorithm.Conservative; + /// /// Gets the value that should be used to seed randomness. /// @@ -506,6 +524,18 @@ public static int GetMaxParallelThreadsOrDefault(this _ITestFrameworkExecutionOp executionOptions.SetValue(TestOptionsNames.Execution.MaxParallelThreads, value); } + /// + /// Sets the parallel algorithm to be used. + /// + public static void SetParallelAlgorithm( + this _ITestFrameworkExecutionOptions executionOptions, + ParallelAlgorithm? value) + { + Guard.ArgumentNotNull(executionOptions); + + executionOptions.SetValue(TestOptionsNames.Execution.ParallelAlgorithm, value.HasValue ? value.GetValueOrDefault().ToString() : null); + } + /// /// Sets the value that should be used to seed randomness. /// diff --git a/src/xunit.v3.runner.common/Frameworks/TestAssemblyConfiguration.cs b/src/xunit.v3.runner.common/Frameworks/TestAssemblyConfiguration.cs index 202a43f78..97d8632b1 100644 --- a/src/xunit.v3.runner.common/Frameworks/TestAssemblyConfiguration.cs +++ b/src/xunit.v3.runner.common/Frameworks/TestAssemblyConfiguration.cs @@ -159,6 +159,16 @@ public class TestAssemblyConfiguration /// public TestMethodDisplayOptions MethodDisplayOptionsOrDefault => MethodDisplayOptions ?? TestMethodDisplayOptions.None; + /// + /// Gets or sets the algorithm to be used for parallelization. + /// + public ParallelAlgorithm? ParallelAlgorithm { get; set; } + + /// + /// Gets or sets the algorithm to be used for parallelization. + /// + public ParallelAlgorithm ParallelAlgorithmOrDefault { get { return ParallelAlgorithm ?? Sdk.ParallelAlgorithm.Conservative; } } + /// /// Gets or sets a flag indicating that this assembly is safe to parallelize against /// other assemblies. diff --git a/src/xunit.v3.runner.common/Frameworks/_TestFrameworkOptions.cs b/src/xunit.v3.runner.common/Frameworks/_TestFrameworkOptions.cs index 58ad956e9..1b9cf0148 100644 --- a/src/xunit.v3.runner.common/Frameworks/_TestFrameworkOptions.cs +++ b/src/xunit.v3.runner.common/Frameworks/_TestFrameworkOptions.cs @@ -26,7 +26,10 @@ public class _TestFrameworkOptions : _ITestFrameworkDiscoveryOptions, _ITestFram } /// - /// Creates an instance of for discovery purposes. + /// Creates an instance of for discovery purposes. Note that this + /// method is primarily for testing purposes and is not guaranteed not to have a stable parameter + /// list across releases. For a stable API, use the overload that takes + /// instead. /// /// Optional value to indicate the culture used for test discovery /// Optional flag to enable diagnostic messages @@ -84,7 +87,10 @@ public static _ITestFrameworkDiscoveryOptions ForDiscovery(TestAssemblyConfigura new _TestFrameworkOptions(optionsJson); /// - /// Creates an instance of for execution purposes. + /// Creates an instance of for execution purposes. Note that this + /// method is primarily for testing purposes and is not guaranteed not to have a stable parameter + /// list across releases. For a stable API, use the overload that takes + /// instead. /// /// Optional value to indicate the culture used for test execution /// Optional flag to enable diagnostic messages @@ -92,6 +98,7 @@ public static _ITestFrameworkDiscoveryOptions ForDiscovery(TestAssemblyConfigura /// Optional flag to indicate how explicit tests should be handled /// Optional flag to enable internal diagnostic messages /// Optional value for maximum threads when running tests in parallel + /// Option value to choose the parallel algorithm /// Optional override value to seed randomization /// Optional flag to indicate that tests should stop running once one test has failed /// @@ -102,6 +109,7 @@ public static _ITestFrameworkDiscoveryOptions ForDiscovery(TestAssemblyConfigura ExplicitOption? explicitOption = null, bool? internalDiagnosticMessages = null, int? maxParallelThreads = null, + ParallelAlgorithm? parallelAlgorithm = null, int? seed = null, bool? stopOnFail = null) { @@ -113,6 +121,7 @@ public static _ITestFrameworkDiscoveryOptions ForDiscovery(TestAssemblyConfigura result.SetExplicitOption(explicitOption); result.SetInternalDiagnosticMessages(internalDiagnosticMessages); result.SetMaxParallelThreads(maxParallelThreads); + result.SetParallelAlgorithm(parallelAlgorithm); result.SetSeed(seed); result.SetStopOnTestFail(stopOnFail); @@ -134,6 +143,7 @@ public static _ITestFrameworkExecutionOptions ForExecution(TestAssemblyConfigura configuration.ExplicitOption, configuration.InternalDiagnosticMessages, configuration.MaxParallelThreads, + configuration.ParallelAlgorithm, configuration.Seed, configuration.StopOnFail ); diff --git a/src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs b/src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs index 128fad6cc..3fc492eee 100644 --- a/src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs +++ b/src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs @@ -81,6 +81,12 @@ public abstract class CommandLineParserBase ); AddParser("noColor", OnNoColor, CommandLineGroup.General, null, "do not output results with colors"); AddParser("noLogo", OnNoLogo, CommandLineGroup.General, null, "do not show the copyright message"); + AddParser("parallelAlgorithm", OnParallelAlgorithm, CommandLineGroup.General, "