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 => { };