Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/TUnit.Core/Contexts/TestRegisteredContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,34 @@ public void SetParallelLimiter(IParallelLimit parallelLimit)
TestContext.ParallelLimiter = _explicitParallelLimiter ?? parallelLimit;
}

/// <summary>
/// Removes the parallel limiter of the test, including a limiter set by a
/// <see cref="ParallelLimiterAttribute{TParallelLimit}"/> or by <see cref="SetParallelLimiter"/>.
/// </summary>
/// <remarks>
/// 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 <see cref="IEventReceiver.Order"/> than the receivers it overrides.
/// </remarks>
public void ClearParallelLimiter()
{
_explicitParallelLimiter = null;
TestContext.ParallelLimiter = null;
}

/// <summary>
/// Removes every parallel constraint of the test, for example the constraints added by
/// <see cref="NotInParallelAttribute"/> and <see cref="ParallelGroupAttribute"/>.
/// The test can then run in parallel with any other test.
/// </summary>
/// <remarks>
/// 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 <see cref="IEventReceiver.Order"/> than the receivers it overrides.
/// </remarks>
public void ClearParallelConstraints()
{
TestContext.ClearParallelConstraints();
}

internal void SetExplicitParallelLimiter(IParallelLimit parallelLimit)
{
_explicitParallelLimiter = parallelLimit;
Expand Down
5 changes: 5 additions & 0 deletions src/TUnit.Core/TestContext.Parallelization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@ void ITestParallelization.AddConstraint(IParallelConstraint constraint)
_parallelConstraints.Add(constraint);
}
}

internal void ClearParallelConstraints()
{
_parallelConstraints?.Clear();
}
}
35 changes: 35 additions & 0 deletions tests/TUnit.Engine.Tests/ClearParallelizationTests.cs
Original file line number Diff line number Diff line change
@@ -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)
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
96 changes: 96 additions & 0 deletions tests/TUnit.TestProject/Bugs/6892/ClearParallelizationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using TUnit.Core.Interfaces;
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._6892;

/// <summary>
/// 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.
/// </summary>
[EngineTest(ExpectedResult.Pass)]
[NotInParallel("Issue6892")]
[ParallelLimiter<Issue6892SerialLimit>]
[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<Issue6892SerialLimit>]
[ClearParallelization]
[SetIssue6892WideLimiter]
public async Task LimiterSetAfterClearIsApplied()
{
await Assert.That(TestContext.Current!.Parallelism.Limiter).IsTypeOf<Issue6892WideLimit>();
}
}

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