11using Serilog . Events ;
22using Serilog . Formatting . Display ;
33using Serilog . Sinks . XUnit . Injectable . Abstract ;
4- using System ;
5- using System . Collections . Generic ;
6- using System . Threading . Channels ;
7- using System . Threading . Tasks ;
84using Soenneker . Extensions . Task ;
95using Soenneker . Extensions . ValueTask ;
106using Soenneker . Utils . AtomicBool ;
117using Soenneker . Utils . ReusableStringWriter ;
8+ using System ;
9+ using System . Collections . Generic ;
10+ using System . Threading ;
11+ using System . Threading . Channels ;
12+ using System . Threading . Tasks ;
1213using Xunit ;
1314using Xunit . Sdk ;
1415using Xunit . v3 ;
1516
1617namespace Serilog . Sinks . XUnit . Injectable ;
1718
18- /// <inheritdoc cref="IInjectableTestOutputSink"/>
19- public sealed class InjectableTestOutputSink : IInjectableTestOutputSink , IAsyncDisposable , IDisposable
19+ ///<inheritdoc cref="IInjectableTestOutputSink"/>
20+ public sealed class InjectableTestOutputSink : IInjectableTestOutputSink
2021{
2122 private const string _defaultTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{Exception}" ;
23+ private const int _backlogCap = 2048 ; // limit in-memory backlog when helper isn't available
24+ private const int _channelCapacity = 4096 ; // apply backpressure under heavy logging
25+
26+ private static readonly TimeSpan _drainWait = TimeSpan . FromSeconds ( 2 ) ;
27+ private static readonly TimeSpan _cancelWait = TimeSpan . FromSeconds ( 3 ) ;
2228
2329 private readonly MessageTemplateTextFormatter _fmt ;
2430
25- private readonly Channel < LogEvent > _ch = Channel . CreateUnbounded < LogEvent > ( new UnboundedChannelOptions
26- { SingleReader = true , SingleWriter = false , AllowSynchronousContinuations = false } ) ;
31+ // Bounded channel prevents infinite growth if tests end or helper is missing.
32+ private readonly Channel < LogEvent > _ch = Channel . CreateBounded < LogEvent > ( new BoundedChannelOptions ( _channelCapacity )
33+ {
34+ SingleReader = true ,
35+ SingleWriter = false ,
36+ FullMode = BoundedChannelFullMode . DropWrite ,
37+ AllowSynchronousContinuations = false
38+ } ) ;
2739
40+ private readonly CancellationTokenSource _cts = new ( ) ;
2841 private readonly Task _readerTask ;
2942
3043 private readonly ReusableStringWriter _sw = new ( ) ;
3144
32- // Shared with producers/reader – volatile for safe publication
45+ // Volatile so producers/reader see latest references without locks
3346 private volatile ITestOutputHelper ? _helper ;
3447 private volatile IMessageSink ? _sink ;
3548
36- // Only the reader touches this; safe without locks.
49+ // Only the reader loop touches this queue
3750 private readonly Queue < LogEvent > _pending = new ( ) ;
3851
3952 private readonly AtomicBool _disposed = new ( ) ;
4053
4154 public InjectableTestOutputSink ( string outputTemplate = _defaultTemplate , IFormatProvider ? formatProvider = null )
4255 {
4356 _fmt = new MessageTemplateTextFormatter ( outputTemplate , formatProvider ) ;
44- _readerTask = Task . Run ( ReadLoop ) ;
57+ _readerTask = Task . Run ( ( ) => ReadLoop ( _cts . Token ) ) ;
4558 }
4659
47- public void Inject ( ITestOutputHelper helper , IMessageSink ? sink = null )
60+ /// <summary>Inject the current test's output helper (call at test start).</summary>
61+ public void Inject ( ITestOutputHelper helper , IMessageSink ? diagnosticSink = null )
4862 {
4963 ArgumentNullException . ThrowIfNull ( helper ) ;
50- _helper = helper ;
51- _sink = sink ;
64+ _helper = helper ; // publish to reader
65+ _sink = diagnosticSink ;
5266 }
5367
68+ /// <summary>Serilog pipeline entry point.</summary>
5469 public void Emit ( LogEvent logEvent )
5570 {
56- if ( _disposed . IsFalse )
57- _ch . Writer . TryWrite ( logEvent ) ;
71+ if ( logEvent is null || _disposed . IsTrue )
72+ return ;
73+
74+ _ch . Writer . TryWrite ( logEvent ) ; // non-blocking; may drop when full
5875 }
5976
60- private async Task ReadLoop ( )
77+ public void Complete ( )
6178 {
62- await foreach ( LogEvent evt in _ch . Reader . ReadAllAsync ( )
63- . ConfigureAwait ( false ) )
64- {
65- ITestOutputHelper ? helper = _helper ; // volatile read
79+ if ( _disposed . IsTrue )
80+ return ;
6681
67- if ( helper is null )
68- {
69- _pending . Enqueue ( evt ) ; // buffer until helper arrives
70- continue ;
71- }
82+ _helper = null ; // stop xUnit writes
83+ _ch . Writer . TryComplete ( ) ; // prefer graceful drain
84+ // optional: don't cancel here; let Dispose handle fallback cancel on timeout
85+ }
7286
73- // first, flush any backlog that accumulated pre-inject
74- while ( _pending . Count > 0 )
87+ private async Task ReadLoop ( CancellationToken ct )
88+ {
89+ try
90+ {
91+ await foreach ( LogEvent evt in _ch . Reader . ReadAllAsync ( ct )
92+ . ConfigureAwait ( false ) )
7593 {
76- Write ( _pending . Dequeue ( ) , helper ) ;
94+ if ( ct . IsCancellationRequested ) break ;
95+
96+ ITestOutputHelper ? helper = _helper ; // volatile read
97+ if ( helper is null )
98+ {
99+ if ( _pending . Count < _backlogCap )
100+ _pending . Enqueue ( evt ) ;
101+ continue ;
102+ }
103+
104+ // Flush any backlog that accumulated before helper arrived
105+ while ( ! ct . IsCancellationRequested && _pending . Count > 0 )
106+ Write ( _pending . Dequeue ( ) , helper ) ;
107+
108+ if ( ! ct . IsCancellationRequested )
109+ Write ( evt , helper ) ;
77110 }
78-
79- Write ( evt , helper ) ;
111+ }
112+ catch ( OperationCanceledException )
113+ {
114+ // expected during teardown
115+ }
116+ catch
117+ {
118+ // never let logging crash tests
80119 }
81120 }
82121
83122 private void Write ( LogEvent evt , ITestOutputHelper helper )
84123 {
85- _sw . Reset ( ) ;
86- _fmt . Format ( evt , _sw ) ;
87- string message = _sw . Finish ( ) ;
88-
89- _sink ? . OnMessage ( new DiagnosticMessage ( message ) ) ;
90-
91124 try
92125 {
93- helper . WriteLine ( message ) ;
126+ _sw . Reset ( ) ;
127+ _fmt . Format ( evt , _sw ) ;
128+ string message = _sw . Finish ( ) ;
129+
130+ try
131+ {
132+ _sink ? . OnMessage ( new DiagnosticMessage ( message ) ) ;
133+ }
134+ catch
135+ {
136+ /* ignore */
137+ }
138+
139+ try
140+ {
141+ helper . WriteLine ( message ) ;
142+ }
143+ catch ( InvalidOperationException )
144+ {
145+ // test finished; helper invalid
146+ _helper = null ;
147+
148+ if ( _pending . Count < _backlogCap )
149+ _pending . Enqueue ( evt ) ;
150+ }
151+ catch
152+ {
153+ _helper = null ;
154+ }
94155 }
95- catch ( InvalidOperationException )
156+ catch
96157 {
97- // Helper became invalid (test finished) – cache the event
98- _helper = null ;
99- _pending . Enqueue ( evt ) ;
158+ // swallow formatting/writing failures
100159 }
101160 }
102161
103162 public async ValueTask DisposeAsync ( )
104163 {
105- if ( ! _disposed . TrySetTrue ( ) )
106- return ;
164+ if ( ! _disposed . TrySetTrue ( ) ) return ;
107165
108- // 1) Tell the reader no more items are coming
109- _ch . Writer . TryComplete ( ) ;
166+ _helper = null ; // stop xUnit calls after this point
167+ _ch . Writer . TryComplete ( ) ; // 1) tell reader: no more items
110168
111- // 2) Let the reader finish formatting & flushing
112- await _readerTask . NoSync ( ) ;
169+ try
170+ {
171+ // 2) give the reader a short window to drain cleanly
172+ await _readerTask . WaitAsync ( _drainWait )
173+ . NoSync ( ) ;
174+ }
175+ catch ( TimeoutException )
176+ {
177+ // 3) fallback: force-break the loop if it didn’t finish
178+ await _cts . CancelAsync ( )
179+ . NoSync ( ) ;
180+ try
181+ {
182+ await _readerTask . WaitAsync ( _cancelWait )
183+ . NoSync ( ) ;
184+ }
185+ catch
186+ {
187+ /* swallow during teardown */
188+ }
189+ }
190+ catch ( OperationCanceledException )
191+ {
192+ /* ok */
193+ }
113194
114- await _sw . DisposeAsync ( )
115- . NoSync ( ) ;
195+ try
196+ {
197+ await _sw . DisposeAsync ( )
198+ . NoSync ( ) ;
199+ }
200+ catch
201+ {
202+ }
203+
204+ _cts . Dispose ( ) ;
116205 }
117206
118207 public void Dispose ( )
119208 {
120- if ( ! _disposed . TrySetTrue ( ) )
121- return ;
209+ if ( ! _disposed . TrySetTrue ( ) ) return ;
122210
211+ _helper = null ;
123212 _ch . Writer . TryComplete ( ) ;
124213
125214 try
@@ -129,9 +218,25 @@ public void Dispose()
129218 }
130219 catch
131220 {
132- // swallow during teardown
221+ _cts . Cancel ( ) ;
222+ try
223+ {
224+ _readerTask . GetAwaiter ( )
225+ . GetResult ( ) ;
226+ }
227+ catch
228+ {
229+ }
230+ }
231+
232+ try
233+ {
234+ _sw . Dispose ( ) ;
235+ }
236+ catch
237+ {
133238 }
134239
135- _sw . Dispose ( ) ;
240+ _cts . Dispose ( ) ;
136241 }
137242}
0 commit comments