diff --git a/src/TUnit.Core/Contexts/TestRegisteredContext.cs b/src/TUnit.Core/Contexts/TestRegisteredContext.cs
index 23a9fe4c88..e9164cfb65 100644
--- a/src/TUnit.Core/Contexts/TestRegisteredContext.cs
+++ b/src/TUnit.Core/Contexts/TestRegisteredContext.cs
@@ -64,6 +64,34 @@ public void SetParallelLimiter(IParallelLimit parallelLimit)
TestContext.ParallelLimiter = _explicitParallelLimiter ?? parallelLimit;
}
+ ///
+ /// Removes the parallel limiter of the test, including a limiter set by a
+ /// or by .
+ ///
+ ///
+ /// Only limiters set before this call are removed. A receiver that runs later can set a limiter again,
+ /// so give the calling receiver a higher than the receivers it overrides.
+ ///
+ public void ClearParallelLimiter()
+ {
+ _explicitParallelLimiter = null;
+ TestContext.ParallelLimiter = null;
+ }
+
+ ///
+ /// Removes every parallel constraint of the test, for example the constraints added by
+ /// and .
+ /// The test can then run in parallel with any other test.
+ ///
+ ///
+ /// Only constraints added before this call are removed. A receiver that runs later can add a constraint again,
+ /// so give the calling receiver a higher than the receivers it overrides.
+ ///
+ public void ClearParallelConstraints()
+ {
+ TestContext.ClearParallelConstraints();
+ }
+
internal void SetExplicitParallelLimiter(IParallelLimit parallelLimit)
{
_explicitParallelLimiter = parallelLimit;
diff --git a/src/TUnit.Core/TestContext.Parallelization.cs b/src/TUnit.Core/TestContext.Parallelization.cs
index 0d14a2cdc4..c53c24f519 100644
--- a/src/TUnit.Core/TestContext.Parallelization.cs
+++ b/src/TUnit.Core/TestContext.Parallelization.cs
@@ -27,4 +27,9 @@ void ITestParallelization.AddConstraint(IParallelConstraint constraint)
_parallelConstraints.Add(constraint);
}
}
+
+ internal void ClearParallelConstraints()
+ {
+ _parallelConstraints?.Clear();
+ }
}
diff --git a/tests/TUnit.Engine.Tests/ClearParallelizationTests.cs b/tests/TUnit.Engine.Tests/ClearParallelizationTests.cs
new file mode 100644
index 0000000000..50f85821ba
--- /dev/null
+++ b/tests/TUnit.Engine.Tests/ClearParallelizationTests.cs
@@ -0,0 +1,35 @@
+using Shouldly;
+using TUnit.Engine.Tests.Enums;
+
+namespace TUnit.Engine.Tests;
+
+public class ClearParallelizationTests(TestMode testMode) : InvokableTestBase(testMode)
+{
+ [Test]
+ public async Task ClearedConstraintsAndLimiter_LetTestsRunTogether()
+ {
+ await RunTestsWithFilter(
+ "/*/TUnit.TestProject.Bugs._6892/ClearParallelizationTests/*",
+ [
+ result => result.ResultSummary.Outcome.ShouldBe("Completed"),
+ result => result.ResultSummary.Counters.Total.ShouldBe(2),
+ result => result.ResultSummary.Counters.Passed.ShouldBe(2),
+ result => result.ResultSummary.Counters.Failed.ShouldBe(0),
+ result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0)
+ ]);
+ }
+
+ [Test]
+ public async Task LimiterSetAfterClear_IsApplied()
+ {
+ await RunTestsWithFilter(
+ "/*/TUnit.TestProject.Bugs._6892/ClearParallelLimiterThenSetTests/*",
+ [
+ result => result.ResultSummary.Outcome.ShouldBe("Completed"),
+ result => result.ResultSummary.Counters.Total.ShouldBe(1),
+ result => result.ResultSummary.Counters.Passed.ShouldBe(1),
+ result => result.ResultSummary.Counters.Failed.ShouldBe(0),
+ result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0)
+ ]);
+ }
+}
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index 84f51a649a..3805d59b96 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -1736,6 +1736,8 @@ namespace
public .TestContext TestContext { get; }
public .TestDetails TestDetails { get; }
public string TestName { get; }
+ public void ClearParallelConstraints() { }
+ public void ClearParallelLimiter() { }
public void SetHookExecutor(. executor) { }
public void SetParallelLimiter(. parallelLimit) { }
public void SetSkipped(string reason) { }
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 5e144790a9..112438aef2 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1736,6 +1736,8 @@ namespace
public .TestContext TestContext { get; }
public .TestDetails TestDetails { get; }
public string TestName { get; }
+ public void ClearParallelConstraints() { }
+ public void ClearParallelLimiter() { }
public void SetHookExecutor(. executor) { }
public void SetParallelLimiter(. parallelLimit) { }
public void SetSkipped(string reason) { }
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index 4aab8703f5..320a7c5e75 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1736,6 +1736,8 @@ namespace
public .TestContext TestContext { get; }
public .TestDetails TestDetails { get; }
public string TestName { get; }
+ public void ClearParallelConstraints() { }
+ public void ClearParallelLimiter() { }
public void SetHookExecutor(. executor) { }
public void SetParallelLimiter(. parallelLimit) { }
public void SetSkipped(string reason) { }
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
index a736384c8b..40b518d320 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -1675,6 +1675,8 @@ namespace
public .TestContext TestContext { get; }
public .TestDetails TestDetails { get; }
public string TestName { get; }
+ public void ClearParallelConstraints() { }
+ public void ClearParallelLimiter() { }
public void SetHookExecutor(. executor) { }
public void SetParallelLimiter(. parallelLimit) { }
public void SetSkipped(string reason) { }
diff --git a/tests/TUnit.TestProject/Bugs/6892/ClearParallelizationTests.cs b/tests/TUnit.TestProject/Bugs/6892/ClearParallelizationTests.cs
new file mode 100644
index 0000000000..45e82ba66f
--- /dev/null
+++ b/tests/TUnit.TestProject/Bugs/6892/ClearParallelizationTests.cs
@@ -0,0 +1,96 @@
+using TUnit.Core.Interfaces;
+using TUnit.TestProject.Attributes;
+
+namespace TUnit.TestProject.Bugs._6892;
+
+///
+/// Regression tests for https://github.com/thomhurst/TUnit/issues/6892.
+/// A registration receiver can remove the parallel constraints and the parallel limiter of a test.
+///
+[EngineTest(ExpectedResult.Pass)]
+[NotInParallel("Issue6892")]
+[ParallelLimiter]
+[ClearParallelization]
+public class ClearParallelizationTests
+{
+ private static readonly TaskCompletionSource FirstStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ private static readonly TaskCompletionSource SecondStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ // See Repro5700 for why the rendezvous deadline is generous.
+ private static readonly TimeSpan RendezvousTimeout = TimeSpan.FromSeconds(60);
+
+ [Test]
+ public async Task First()
+ {
+ await AssertNoParallelization();
+
+ // Both tests share a NotInParallel key and a limit of 1. They finish only if they run at the same time.
+ FirstStarted.TrySetResult();
+ using var cts = new CancellationTokenSource(RendezvousTimeout);
+ await SecondStarted.Task.WaitAsync(cts.Token);
+ }
+
+ [Test]
+ public async Task Second()
+ {
+ await AssertNoParallelization();
+
+ SecondStarted.TrySetResult();
+ using var cts = new CancellationTokenSource(RendezvousTimeout);
+ await FirstStarted.Task.WaitAsync(cts.Token);
+ }
+
+ private static async Task AssertNoParallelization()
+ {
+ var parallelism = TestContext.Current!.Parallelism;
+ await Assert.That(parallelism.Constraints).IsEmpty();
+ await Assert.That(parallelism.Limiter).IsNull();
+ }
+}
+
+[EngineTest(ExpectedResult.Pass)]
+public class ClearParallelLimiterThenSetTests
+{
+ [Test]
+ [ParallelLimiter]
+ [ClearParallelization]
+ [SetIssue6892WideLimiter]
+ public async Task LimiterSetAfterClearIsApplied()
+ {
+ await Assert.That(TestContext.Current!.Parallelism.Limiter).IsTypeOf();
+ }
+}
+
+internal sealed class ClearParallelizationAttribute : Attribute, ITestRegisteredEventReceiver
+{
+ // Runs after ParallelLimiterAttribute (Order 0).
+ public int Order => 100;
+
+ public ValueTask OnTestRegistered(TestRegisteredContext context)
+ {
+ context.ClearParallelConstraints();
+ context.ClearParallelLimiter();
+ return default;
+ }
+}
+
+internal sealed class SetIssue6892WideLimiterAttribute : Attribute, ITestRegisteredEventReceiver
+{
+ public int Order => 200;
+
+ public ValueTask OnTestRegistered(TestRegisteredContext context)
+ {
+ context.SetParallelLimiter(new Issue6892WideLimit());
+ return default;
+ }
+}
+
+public sealed class Issue6892SerialLimit : IParallelLimit
+{
+ public int Limit => 1;
+}
+
+public sealed class Issue6892WideLimit : IParallelLimit
+{
+ public int Limit => 8;
+}