11using System ;
2- using System . Collections . Generic ;
3- using System . IO ;
2+ using System . Collections . Concurrent ;
3+ using System . Text ;
4+ using System . Threading ;
45using Serilog . Events ;
56using Serilog . Formatting . Display ;
67using Serilog . Sinks . XUnit . Injectable . Abstract ;
1011
1112namespace Serilog . Sinks . XUnit . Injectable ;
1213
13- /// <inheritdoc cref="IInjectableTestOutputSink"/>
14+ ///<inheritdoc cref="IInjectableTestOutputSink"/>
1415public sealed class InjectableTestOutputSink : IInjectableTestOutputSink
1516{
16- private readonly Stack < LogEvent > _cachedLogEvents ;
17- private readonly MessageTemplateTextFormatter _textFormatter ;
18- private IMessageSink ? _messageSink ;
19- private ITestOutputHelper ? _testOutputHelper ;
20-
21- private const string _defaultConsoleOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" ;
22-
23- /// <summary>
24- /// Use this ctor for injecting into the DI container
25- /// </summary>
26- /// <param name="outputTemplate">A message template describing the format used to write to the sink.
27- /// the default is <code>"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"</code>.</param>
28- /// <param name="formatProvider">Supplies culture-specific formatting information, or null.</param>
29- /// <returns>Configuration object allowing method chaining.</returns>
30- public InjectableTestOutputSink ( string outputTemplate = _defaultConsoleOutputTemplate , IFormatProvider ? formatProvider = null )
31- {
32- _cachedLogEvents = new Stack < LogEvent > ( ) ;
17+ private readonly Lock _lock = new ( ) ; // guards flush+write
18+ private readonly ConcurrentQueue < LogEvent > _cache = new ( ) ;
19+ private readonly MessageTemplateTextFormatter _fmt ;
20+
21+ private volatile ITestOutputHelper ? _helper ; // set once, then read
22+ private volatile IMessageSink ? _sink ;
23+
24+ private const string _defaultTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{Exception}" ;
25+
26+ [ ThreadStatic ] private static StringBuilder ? _sb ;
3327
34- _textFormatter = new MessageTemplateTextFormatter ( outputTemplate , formatProvider ) ;
28+ [ ThreadStatic ] private static ReusableStringWriter ? _sw ;
29+
30+ public InjectableTestOutputSink ( string outputTemplate = _defaultTemplate , IFormatProvider ? formatProvider = null )
31+ {
32+ _fmt = new MessageTemplateTextFormatter ( outputTemplate , formatProvider ) ;
3533 }
3634
37- public void Inject ( ITestOutputHelper testOutputHelper , IMessageSink ? messageSink = null )
35+ public void Inject ( ITestOutputHelper helper , IMessageSink ? sink = null )
3836 {
39- _testOutputHelper = testOutputHelper ;
40- _messageSink = messageSink ;
37+ ArgumentNullException . ThrowIfNull ( helper ) ;
38+
39+ // one short, guaranteed exit scope
40+ using Lock . Scope _ = _lock . EnterScope ( ) ;
41+
42+ _helper = helper ;
43+ _sink = sink ;
44+ FlushLocked ( ) ;
4145 }
4246
4347 public void Emit ( LogEvent logEvent )
4448 {
45- if ( _testOutputHelper == null )
49+ ArgumentNullException . ThrowIfNull ( logEvent ) ;
50+
51+ // FAST-PATH: helper not yet available – enqueue without locking
52+ ITestOutputHelper ? helperSnapshot = _helper ; // direct volatile read
53+
54+ if ( helperSnapshot is null )
4655 {
47- _cachedLogEvents . Push ( logEvent ) ;
56+ _cache . Enqueue ( logEvent ) ;
4857 return ;
4958 }
5059
51- FlushCachedLogEvents ( ) ;
52- Write ( logEvent ) ;
60+
61+ using Lock . Scope _ = _lock . EnterScope ( ) ;
62+
63+ // Flush anything another thread buffered before we obtained the scope
64+ FlushLocked ( ) ;
65+ WriteLocked ( logEvent ) ;
5366 }
5467
55- private void FlushCachedLogEvents ( )
68+ private void FlushLocked ( )
5669 {
57- while ( _cachedLogEvents . Count > 0 )
58- {
59- Write ( _cachedLogEvents . Pop ( ) ) ;
60- }
70+ while ( _cache . TryDequeue ( out LogEvent ? queued ) )
71+ WriteLocked ( queued ) ;
6172 }
6273
63- /// <summary>
64- /// Emits the provided log event from a sink
65- /// </summary>
66- /// <param name="logEvent">The event being logged</param>
67- private void Write ( LogEvent logEvent )
74+ private void WriteLocked ( LogEvent evt )
6875 {
69- if ( logEvent == null )
70- throw new ArgumentNullException ( nameof ( logEvent ) ) ;
76+ // rent / reset thread-local buffers
77+ StringBuilder sb = _sb ??= new StringBuilder ( 256 ) ;
78+ sb . Clear ( ) ;
7179
72- var renderSpace = new StringWriter ( ) ;
73- _textFormatter . Format ( logEvent , renderSpace ) ;
80+ ReusableStringWriter sw = _sw ??= new ReusableStringWriter ( sb ) ;
81+ sw . Reset ( ) ;
7482
75- string message = renderSpace . ToString ( ) . Trim ( ) ;
83+ _fmt . Format ( evt , sw ) ;
84+ var message = sb . ToString ( ) ; // single unavoidable alloc
7685
77- _messageSink ? . OnMessage ( new DiagnosticMessage ( message ) ) ;
86+ _sink ? . OnMessage ( new DiagnosticMessage ( message ) ) ;
7887
7988 try
8089 {
81- _testOutputHelper ? . WriteLine ( message ) ;
90+ _helper ? . WriteLine ( message ) ;
8291 }
83- catch ( InvalidOperationException ex )
92+ catch ( InvalidOperationException )
8493 {
85- // Typically no test is active
94+ /* test already finished – swallow */
8695 }
8796 }
8897}
0 commit comments