diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7085117ec..d23ef90d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,11 @@
# [vNext]
## Improvements:
+* Refactor: Introduce virtual timeout properties FormattersBase class to allow individual formatters to override the defaults
## Bug fixes:
-*Contributors of this release (in alphabetical order):*
+*Contributors of this release (in alphabetical order):* @clrudolphi
# v3.3.3 - 2026-01-27
diff --git a/Reqnroll/Formatters/FormatterBase.cs b/Reqnroll/Formatters/FormatterBase.cs
index c152ee6e7..fb618d099 100644
--- a/Reqnroll/Formatters/FormatterBase.cs
+++ b/Reqnroll/Formatters/FormatterBase.cs
@@ -35,6 +35,16 @@ public abstract class FormatterBase : ICucumberMessageFormatter, IDisposable
public string Name => _pluginName;
+ ///
+ /// Gets the timeout duration to wait for formatter task completion during disposal.
+ ///
+ protected virtual TimeSpan DisposeTimeout => TimeSpan.FromSeconds(15);
+
+ ///
+ /// Gets the timeout duration to wait after cancellation during disposal.
+ ///
+ protected virtual TimeSpan DisposeCancellationTimeout => TimeSpan.FromSeconds(15);
+
protected FormatterBase(IFormattersConfigurationProvider configurationProvider, IFormatterLog logger, string pluginName)
{
_configurationProvider = configurationProvider;
@@ -134,7 +144,7 @@ public virtual void Dispose()
Logger.WriteMessage($"DEBUG: Formatters: Dispose is waiting on the formatter task {Name}.");
// In this situation, the TestEngine is shutting down and has called Dispose on the global container.
// Forcing the Dispose to wait until the formatter has had a chance to complete.
- var timeoutTask = Task.Delay(TimeSpan.FromSeconds(15));
+ var timeoutTask = Task.Delay(DisposeTimeout);
var finishedTask = Task.WhenAny(timeoutTask, _formatterTask).GetAwaiter().GetResult();
if (finishedTask == timeoutTask)
{
@@ -151,7 +161,7 @@ public virtual void Dispose()
_logger.WriteMessage($"DEBUG: Formatters:PluginBase.Dispose - cancellation message can't be sent as the collection is closed.");
}
_logger.WriteMessage($"DEBUG: Formatters.PluginBase.Dispose - waiting again after cancellation.");
- timeoutTask = Task.Delay(TimeSpan.FromSeconds(15));
+ timeoutTask = Task.Delay(DisposeCancellationTimeout);
finishedTask = Task.WhenAny(timeoutTask, _formatterTask).GetAwaiter().GetResult();
if (finishedTask == timeoutTask)
{
diff --git a/Tests/Reqnroll.RuntimeTests/Formatters/FileWritingFormatterBaseTests.cs b/Tests/Reqnroll.RuntimeTests/Formatters/FileWritingFormatterBaseTests.cs
index 89132f6f1..e5f99688a 100644
--- a/Tests/Reqnroll.RuntimeTests/Formatters/FileWritingFormatterBaseTests.cs
+++ b/Tests/Reqnroll.RuntimeTests/Formatters/FileWritingFormatterBaseTests.cs
@@ -39,6 +39,9 @@ private class TestFileWritingFormatter : FileWritingFormatterBase
public bool FinalizeInitializationCalled = false;
public Stream? LastStream;
+ protected override TimeSpan DisposeTimeout => TimeSpan.FromMilliseconds(100);
+ protected override TimeSpan DisposeCancellationTimeout => TimeSpan.FromMilliseconds(100);
+
public TestFileWritingFormatter(IFormattersConfigurationProvider config, IFormatterLog logger, IFileSystem fileSystem)
: base(config, logger, fileSystem, "testPlugin", ".txt", "default.txt") { }
diff --git a/Tests/Reqnroll.RuntimeTests/Formatters/FormatterBaseTests.cs b/Tests/Reqnroll.RuntimeTests/Formatters/FormatterBaseTests.cs
index 9306179c2..9ccb3118b 100644
--- a/Tests/Reqnroll.RuntimeTests/Formatters/FormatterBaseTests.cs
+++ b/Tests/Reqnroll.RuntimeTests/Formatters/FormatterBaseTests.cs
@@ -28,6 +28,8 @@ private class TestFormatter : FormatterBase
public bool ReportInitializedCalled = false;
public bool CloseAsyncCalled = false;
public bool CompleteWriterOnLaunchInner = false;
+ protected override TimeSpan DisposeTimeout => TimeSpan.FromMilliseconds(100);
+ protected override TimeSpan DisposeCancellationTimeout => TimeSpan.FromMilliseconds(100);
public TestFormatter(IFormattersConfigurationProvider config, IFormatterLog logger, string name)
: base(config, logger, name) { }