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; +}