diff --git a/CHANGELOG.md b/CHANGELOG.md index 7085117ec..7739a4a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # [vNext] ## Improvements: - +* Formatters: Made shutdown an explicit part of the interfaces for the Broker and Formatters. +* ## 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..b45d5959d 100644 --- a/Reqnroll/Formatters/FormatterBase.cs +++ b/Reqnroll/Formatters/FormatterBase.cs @@ -91,25 +91,23 @@ public async Task PublishAsync(Envelope message) return; } - await PostedMessages.Writer.WriteAsync(message); - - // If the _publisher sends the TestRunFinished message, then we can safely shut down. - if (message.Content() is TestRunFinished) + if (!PostedMessages.Writer.TryWrite(message)) { - _logger.WriteMessage($"DEBUG: Formatters.Plugin {Name} has received the TestRunFinished message and is calling CloseAsync"); - await CloseAsync(); + Logger.WriteMessage($"Cannot add message {message.Content().GetType().Name} to formatter {Name} - channel is no longer accepting messages."); + return; } } protected abstract Task ConsumeAndFormatMessagesBackgroundTask(CancellationToken cancellationToken); - internal async Task CloseAsync() + public async Task CloseAsync() { Logger.WriteMessage($"DEBUG: Formatters:PluginBase.Close called on formatter {Name}; formatter task was launched: {_formatterTask != null}"); if (PostedMessages.Reader.Completion.IsCompleted || _formatterTask!.IsCompleted) throw new InvalidOperationException($"Formatter {Name} has invoked Close when it is already in a closed state."); + Closed = true; PostedMessages.Writer.Complete(); Logger.WriteMessage($"DEBUG: Formatters:PluginBase {Name} has signaled the Channel is closed. Awaiting the writing task."); diff --git a/Reqnroll/Formatters/ICucumberMessageFormatter.cs b/Reqnroll/Formatters/ICucumberMessageFormatter.cs index 200a91363..a88e8c154 100644 --- a/Reqnroll/Formatters/ICucumberMessageFormatter.cs +++ b/Reqnroll/Formatters/ICucumberMessageFormatter.cs @@ -9,4 +9,5 @@ public interface ICucumberMessageFormatter void LaunchFormatter(ICucumberMessageBroker broker); string Name { get; } Task PublishAsync(Envelope message); + Task CloseAsync(); } \ No newline at end of file diff --git a/Reqnroll/Formatters/PubSub/CucumberMessageBroker.cs b/Reqnroll/Formatters/PubSub/CucumberMessageBroker.cs index 91cfe2540..3b8c135ee 100644 --- a/Reqnroll/Formatters/PubSub/CucumberMessageBroker.cs +++ b/Reqnroll/Formatters/PubSub/CucumberMessageBroker.cs @@ -104,4 +104,21 @@ public async Task PublishAsync(Envelope message) } } + public async Task CompleteAsync() + { + _logger.WriteMessage("DEBUG: Formatters - Broker: CompleteAsync called. Closing all active formatters."); + foreach (var formatter in _activeFormatters.Values) + { + try + { + await formatter.CloseAsync(); + } + catch (System.Exception e) + { + _logger.WriteMessage($"Formatters Broker: Exception closing Formatter Plugin {formatter.Name}: {e.Message}"); + } + } + _logger.WriteMessage("DEBUG: Formatters - Broker: CompleteAsync finished."); + } + } \ No newline at end of file diff --git a/Reqnroll/Formatters/PubSub/CucumberMessagePublisher.cs b/Reqnroll/Formatters/PubSub/CucumberMessagePublisher.cs index 13ccd0694..575fb6c33 100644 --- a/Reqnroll/Formatters/PubSub/CucumberMessagePublisher.cs +++ b/Reqnroll/Formatters/PubSub/CucumberMessagePublisher.cs @@ -194,6 +194,9 @@ internal async Task PublisherTestRunCompleteAsync(TestRunFinishedEvent testRunFi await _broker.PublishAsync(Envelope.Create(MessageFactory.ToTestRunFinished(TestRunPassed, _clock.GetNowDateAndTime(), _testRunStartedId))); _logger.WriteMessage("DEBUG: Formatter:Publisher.TestRunComplete: TestRunFinished Message written"); + + await _broker.CompleteAsync(); + _logger.WriteMessage("DEBUG: Formatter:Publisher.TestRunComplete: Broker CompleteAsync finished"); } #region TestThreadExecutionEventPublisher Event Handling Methods diff --git a/Reqnroll/Formatters/PubSub/ICucumberMessageBroker.cs b/Reqnroll/Formatters/PubSub/ICucumberMessageBroker.cs index 8a2d9c654..6de02aa81 100644 --- a/Reqnroll/Formatters/PubSub/ICucumberMessageBroker.cs +++ b/Reqnroll/Formatters/PubSub/ICucumberMessageBroker.cs @@ -1,4 +1,6 @@ -namespace Reqnroll.Formatters.PubSub; +using System.Threading.Tasks; + +namespace Reqnroll.Formatters.PubSub; public interface ICucumberMessageBroker : IMessagePublisher { @@ -6,4 +8,5 @@ public interface ICucumberMessageBroker : IMessagePublisher void Initialize(); void FormatterInitialized(ICucumberMessageFormatter formatterSink, bool enabled); + Task CompleteAsync(); } diff --git a/Tests/Reqnroll.RuntimeTests/Formatters/FormatterBaseTests.cs b/Tests/Reqnroll.RuntimeTests/Formatters/FormatterBaseTests.cs index 9306179c2..9dab0fc3f 100644 --- a/Tests/Reqnroll.RuntimeTests/Formatters/FormatterBaseTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Formatters/FormatterBaseTests.cs @@ -90,14 +90,33 @@ public void LaunchFormatter_Enabled_Calls_LaunchInner_And_StartsTask() } [Fact] - public async Task PublishAsync_Writes_Message_And_Closes_On_TestRunFinished() + public async Task PublishAsync_Writes_TestRunFinished_Without_Triggering_Shutdown() { _configMock.Setup(c => c.Enabled).Returns(true); _configMock.Setup(c => c.GetFormatterConfigurationByName("testPlugin")).Returns(new Dictionary()); _sut.LaunchFormatter(_brokerMock.Object); var msg = Envelope.Create(new TestRunFinished("", false, new Timestamp(0, 0), null, "")); await _sut.PublishAsync(msg); + + // TestRunFinished is treated like any other message — no implicit shutdown _sut.ConsumedMessages.Should().Contain(msg); + + // Formatter is still open; explicit CloseAsync is required + await _sut.CloseAsync(); + } + + [Fact] + public async Task PublishAsync_Logs_When_Channel_No_Longer_Accepting() + { + _configMock.Setup(c => c.Enabled).Returns(true); + _configMock.Setup(c => c.GetFormatterConfigurationByName("testPlugin")).Returns(new Dictionary()); + _sut.LaunchFormatter(_brokerMock.Object); + await _sut.CloseAsync(); + + // Channel is now completed; PublishAsync should log gracefully via TryWrite failure + var msg = Envelope.Create(new TestRunStarted(new Timestamp(0, 0), "")); + await _sut.PublishAsync(msg); + _loggerMock.Verify(l => l.WriteMessage(It.Is(s => s.Contains("formatter is closed"))), Times.AtLeastOnce); } [Fact]