From 2f2aa866f133898fc13b388cbd206349ffff03d5 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Wed, 9 Sep 2026 16:11:26 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Bound=20acceptance=20tests=20by=20t?= =?UTF-8?q?he=20test's=20CancelAfter=20budget=20instead=20of=20the=2090=20?= =?UTF-8?q?s=20Done=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NServiceBus.AcceptanceTesting caps Done at a fixed 90 seconds unless Run is handed a cancellable token. The multi-step journey tests regularly exceed that on Windows runners, where three embedded RavenDB servers share four cores, and fail with a timeout while still making progress. NServiceBusAcceptanceTest now carries [CancelAfter(180_000)], which NUnit applies to every derived fixture through the execution context, and Define wraps the scenario so that Run() without a token uses the test's own. A test that already declares [CancelAfter] keeps its value, and the failure message names the budget that ran out instead of the framework's infinite limit. --- .../NServiceBusAcceptanceTest.cs | 1 + .../TestTimeoutScenario.cs | 109 ++++++++++++++++++ .../TestSupport/AcceptanceTest.cs | 3 +- .../AcceptanceTest.cs | 3 +- .../AcceptanceTest.cs | 3 +- .../AcceptanceTest.cs | 3 +- 6 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 src/ServiceControl.AcceptanceTesting/TestTimeoutScenario.cs diff --git a/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs b/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs index b510e326ec..93614c2dfb 100644 --- a/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs +++ b/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs @@ -13,6 +13,7 @@ /// [TestFixture] [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] + [CancelAfter(180_000)] public abstract partial class NServiceBusAcceptanceTest { [SetUp] diff --git a/src/ServiceControl.AcceptanceTesting/TestTimeoutScenario.cs b/src/ServiceControl.AcceptanceTesting/TestTimeoutScenario.cs new file mode 100644 index 0000000000..c86d6d01bc --- /dev/null +++ b/src/ServiceControl.AcceptanceTesting/TestTimeoutScenario.cs @@ -0,0 +1,109 @@ +namespace ServiceControl.AcceptanceTesting; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus.AcceptanceTesting; +using NServiceBus.AcceptanceTesting.Support; +using NUnit.Framework; +using NUnit.Framework.Internal; + +public static class TestTimeoutScenarioExtensions +{ + /// + /// Bounds the scenario by the test's own cancellation token instead of the framework's fixed 90 second + /// Done limit. The token comes from , which + /// applies to every test and which a test can override. + /// + public static IScenarioWithEndpointBehavior WithTestTimeout(this IScenarioWithEndpointBehavior scenario) + where TContext : ScenarioContext => + new TestTimeoutScenario(scenario); +} + +class TestTimeoutScenario(IScenarioWithEndpointBehavior inner) : IScenarioWithEndpointBehavior + where TContext : ScenarioContext +{ + public IScenarioWithEndpointBehavior WithEndpoint() where T : EndpointConfigurationBuilder, new() + { + inner = inner.WithEndpoint(); + return this; + } + + public IScenarioWithEndpointBehavior WithEndpoint(Action> behavior) where T : EndpointConfigurationBuilder, new() + { + inner = inner.WithEndpoint(behavior); + return this; + } + + public IScenarioWithEndpointBehavior WithEndpoint(EndpointConfigurationBuilder endpointConfigurationBuilder, Action> defineBehavior) + { + inner = inner.WithEndpoint(endpointConfigurationBuilder, defineBehavior); + return this; + } + + public IScenarioWithEndpointBehavior WithComponent(IComponentBehavior componentBehavior) + { + inner = inner.WithComponent(componentBehavior); + return this; + } + + public IScenarioWithEndpointBehavior WithServices(Action configureServices) + { + inner = inner.WithServices(configureServices); + return this; + } + + public IScenarioWithEndpointBehavior WithServices(Action configureServices) + { + inner = inner.WithServices(configureServices); + return this; + } + + public IScenarioWithEndpointBehavior WithServiceResolve(Func resolve, ServiceResolveMode resolveMode = ServiceResolveMode.BeforeStart) + { + inner = inner.WithServiceResolve(resolve, resolveMode); + return this; + } + + public IScenarioWithEndpointBehavior WithServiceResolve(Func resolve, ServiceResolveMode resolveMode = ServiceResolveMode.BeforeStart) + { + inner = inner.WithServiceResolve(resolve, resolveMode); + return this; + } + + public IScenarioWithEndpointBehavior Done(Func func) + { + inner = inner.Done(func); + return this; + } + + public IScenarioWithEndpointBehavior Done(Func func) + { + inner = inner.Done(func); + return this; + } + + public Task Run(CancellationToken cancellationToken = default) => Run(new RunSettings(), cancellationToken); + + public async Task Run(RunSettings settings, CancellationToken cancellationToken = default) + { + // The framework only lifts its fixed 90 second Done limit when handed a token that can actually be + // cancelled, so a caller that passes nothing gets the test's own token instead. + if (!cancellationToken.CanBeCanceled) + { + cancellationToken = TestContext.CurrentContext.CancellationToken; + } + + try + { + return await inner.Run(settings, cancellationToken); + } + catch (Exception e) when (e is TimeoutException or OperationCanceledException && cancellationToken.IsCancellationRequested) + { + // Done wraps the cancellation in a TimeoutException naming the framework's own limit, which is + // infinite for a cancellable token and prints as a negative number of seconds. + throw new TimeoutException($"The scenario did not complete within the test's CancelAfter budget of {TestExecutionContext.CurrentContext.TestCaseTimeout} ms.", e); + } + } +} diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs index a445576e0a..e86af5c88c 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs @@ -90,7 +90,8 @@ protected void ExecuteWhen(Func execute, Func action, protected IScenarioWithEndpointBehavior Define(Action contextInitializer) where T : ScenarioContext, new() => Scenario.Define(contextInitializer) - .WithComponent(serviceControlRunnerBehavior); + .WithComponent(serviceControlRunnerBehavior) + .WithTestTimeout(); protected Action CustomConfiguration = _ => { }; protected Action SetSettings = _ => { }; diff --git a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs index 133e7e805a..5c295cb104 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs @@ -65,7 +65,8 @@ public Task Teardown() protected IScenarioWithEndpointBehavior Define(Action contextInitializer) where T : ScenarioContext, new() => Scenario.Define(contextInitializer) - .WithComponent(serviceControlRunnerBehavior); + .WithComponent(serviceControlRunnerBehavior) + .WithTestTimeout(); protected Action CustomConfiguration = _ => { }; protected Action SetSettings = _ => { }; diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.Monitoring.AcceptanceTests/AcceptanceTest.cs index aaf1849fd2..baf11934be 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/AcceptanceTest.cs @@ -55,7 +55,8 @@ public void Teardown() protected IScenarioWithEndpointBehavior Define(Action contextInitializer) where T : ScenarioContext, new() => Scenario.Define(contextInitializer) - .WithComponent(serviceControlRunnerBehavior); + .WithComponent(serviceControlRunnerBehavior) + .WithTestTimeout(); protected Action CustomConfiguration = _ => { }; protected Action SetSettings = _ => { }; diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs index f618503256..fd5c5301ac 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs @@ -71,7 +71,8 @@ public void Teardown() protected IScenarioWithEndpointBehavior Define(Action contextInitializer) where T : ScenarioContext, new() => Scenario.Define(contextInitializer) - .WithComponent(serviceControlRunnerBehavior); + .WithComponent(serviceControlRunnerBehavior) + .WithTestTimeout(); protected Action CustomPrimaryEndpointConfiguration = c => { }; protected Action CustomAuditEndpointConfiguration = c => { };