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.52.0.31.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, "