88using NetEvolve . Arguments ;
99using NetEvolve . Logging . Abstractions ;
1010using Xunit . Abstractions ;
11+ using Xunit . Sdk ;
1112
1213/// <summary>
1314/// Represents a logger that writes messages to xunit output.
1415/// </summary>
1516public class XUnitLogger : ILogger , ISupportExternalScope
1617{
1718 private readonly IXUnitLoggerOptions _options ;
18- private readonly ITestOutputHelper _testOutputHelper ;
1919 private readonly TimeProvider _timeProvider ;
2020
2121 private readonly List < LoggedMessage > _loggedMessages ;
2222
23+ private readonly Action < string > _writeToLog ;
24+
2325 private const int DefaultCapacity = 1024 ;
2426
2527 [ ThreadStatic ]
@@ -33,6 +35,43 @@ public class XUnitLogger : ILogger, ISupportExternalScope
3335 /// <inheritdoc cref="IHasLoggedMessages.LoggedMessages"/>
3436 public IReadOnlyList < LoggedMessage > LoggedMessages => _loggedMessages . AsReadOnly ( ) ;
3537
38+ /// <summary>
39+ /// Creates a new instance of <see cref="XUnitLogger"/>.
40+ /// </summary>
41+ /// <param name="messageSink">The <see cref="IMessageSink" /> to write the log messages to.</param>
42+ /// <param name="timeProvider">The <see cref="TimeProvider" /> to use to get the current time.</param>
43+ /// <param name="scopeProvider">The <see cref="IExternalScopeProvider" /> to use to get the current scope.</param>
44+ /// <param name="options">The options to control the behavior of the logger.</param>
45+ /// <returns>A cached or new instance of <see cref="XUnitLogger"/>.</returns>
46+ public static XUnitLogger CreateLogger (
47+ IMessageSink messageSink ,
48+ TimeProvider timeProvider ,
49+ IExternalScopeProvider ? scopeProvider = null ,
50+ IXUnitLoggerOptions ? options = null
51+ )
52+ {
53+ Argument . ThrowIfNull ( messageSink ) ;
54+
55+ return new XUnitLogger ( messageSink , timeProvider , scopeProvider , options ) ;
56+ }
57+
58+ /// <summary>
59+ /// Creates a new instance of <see cref="XUnitLogger{T}"/>.
60+ /// </summary>
61+ /// <typeparam name="T">The type who's fullname is used as the category name for messages produced by the logger.</typeparam>
62+ /// <param name="messageSink">The <see cref="IMessageSink" /> to write the log messages to.</param>
63+ /// <param name="timeProvider">The <see cref="TimeProvider" /> to use to get the current time.</param>
64+ /// <param name="scopeProvider">The <see cref="IExternalScopeProvider" /> to use to get the current scope.</param>
65+ /// <param name="options">The options to control the behavior of the logger.</param>
66+ /// <returns>A cached or new instance of <see cref="XUnitLogger"/>.</returns>
67+ public static XUnitLogger < T > CreateLogger < T > (
68+ IMessageSink messageSink ,
69+ TimeProvider timeProvider ,
70+ IExternalScopeProvider ? scopeProvider = null ,
71+ IXUnitLoggerOptions ? options = null
72+ )
73+ where T : notnull => new XUnitLogger < T > ( messageSink , timeProvider , scopeProvider , options ) ;
74+
3675 /// <summary>
3776 /// Creates a new instance of <see cref="XUnitLogger"/>.
3877 /// </summary>
@@ -82,11 +121,31 @@ private protected XUnitLogger(
82121 Argument . ThrowIfNull ( timeProvider ) ;
83122
84123 ScopeProvider = scopeProvider ?? NullExternalScopeProvider . Instance ;
85- _testOutputHelper = testOutputHelper ;
86124 _timeProvider = timeProvider ;
87125 _options = options ?? XUnitLoggerOptions . Default ;
88126
89127 _loggedMessages = [ ] ;
128+
129+ _writeToLog = testOutputHelper . WriteLine ;
130+ }
131+
132+ private protected XUnitLogger (
133+ IMessageSink messageSink ,
134+ TimeProvider timeProvider ,
135+ IExternalScopeProvider ? scopeProvider ,
136+ IXUnitLoggerOptions ? options
137+ )
138+ {
139+ Argument . ThrowIfNull ( messageSink ) ;
140+ Argument . ThrowIfNull ( timeProvider ) ;
141+
142+ ScopeProvider = scopeProvider ?? NullExternalScopeProvider . Instance ;
143+ _timeProvider = timeProvider ;
144+ _options = options ?? XUnitLoggerOptions . Default ;
145+
146+ _loggedMessages = [ ] ;
147+
148+ _writeToLog = message => _ = messageSink . OnMessage ( new DiagnosticMessage ( message ) ) ;
90149 }
91150
92151 /// <inheritdoc cref="ILogger.BeginScope{TState}(TState)"/>
@@ -112,88 +171,84 @@ public void Log<TState>(
112171 return ;
113172 }
114173
115- var builder = _builder ;
116- _builder = null ;
117- builder ??= new StringBuilder ( DefaultCapacity ) ;
118-
119174 try
120175 {
121176 var message = formatter ( state , exception ) ;
122177 var now = _timeProvider . GetLocalNow ( ) ;
123- ( builder , var scopes ) = CreateMessage (
124- logLevel ,
125- state ,
126- exception ,
127- builder ,
128- message ,
129- now
130- ) ;
178+ var ( fullMessage , scopes ) = CreateMessage ( logLevel , state , exception , message , now ) ;
131179
132180 _loggedMessages . Add (
133181 new LoggedMessage ( now , logLevel , eventId , message , exception , scopes )
134182 ) ;
135- _testOutputHelper . WriteLine ( builder . ToString ( ) ) ;
183+
184+ _writeToLog . Invoke ( fullMessage ) ;
136185 }
137186 catch
138187 {
139188 // Ignore exception.
140189 // Unfortunately, this can happen if the process is terminated before the end of the test.
141190 }
142- finally
143- {
144- _ = builder . Clear ( ) ;
145- if ( builder . Capacity > DefaultCapacity )
146- {
147- builder . Capacity = DefaultCapacity ;
148- }
149- _builder = builder ;
150- }
151191 }
152192
153- private ( StringBuilder , List < object ? > ) CreateMessage < TState > (
193+ private ( string , List < object ? > ) CreateMessage < TState > (
154194 LogLevel logLevel ,
155195 TState state ,
156196 Exception ? exception ,
157- StringBuilder builder ,
158197 string message ,
159198 DateTimeOffset now
160199 )
161200 {
162201 var scopes = new List < object ? > ( ) ;
163- if ( ! _options . DisableTimestamp )
164- {
165- _ = builder
166- . Append ( now . ToString ( _options . TimestampFormat , CultureInfo . InvariantCulture ) )
167- . Append ( ' ' ) ;
168- }
202+ var builder = _builder ;
203+ _builder = null ;
204+ builder ??= new StringBuilder ( DefaultCapacity ) ;
169205
170- if ( ! _options . DisableLogLevel )
206+ try
171207 {
172- _ = builder . Append ( '[' ) . Append ( LogLevelToString ( logLevel ) ) . Append ( "] " ) ;
173- }
208+ if ( ! _options . DisableTimestamp )
209+ {
210+ _ = builder
211+ . Append ( now . ToString ( _options . TimestampFormat , CultureInfo . InvariantCulture ) )
212+ . Append ( ' ' ) ;
213+ }
174214
175- _ = builder . Append ( message ) ;
215+ if ( ! _options . DisableLogLevel )
216+ {
217+ _ = builder . Append ( '[' ) . Append ( LogLevelToString ( logLevel ) ) . Append ( "] " ) ;
218+ }
176219
177- if ( exception is not null )
178- {
179- _ = builder . Append ( '\n ' ) . Append ( exception ) ;
180- }
220+ _ = builder . Append ( message ) ;
181221
182- if (
183- ! _options . DisableAdditionalInformation
184- && state is IReadOnlyList < KeyValuePair < string , object ? > > additionalInformation
185- )
186- {
187- _ = builder . Append ( '\n ' ) . Append ( '\t ' ) . Append ( "Additional Information" ) ;
188- foreach ( var info in additionalInformation )
222+ if ( exception is not null )
189223 {
190- AddAdditionalInformation ( builder , info ) ;
224+ _ = builder . Append ( ' \n ' ) . Append ( exception ) ;
191225 }
192- }
193226
194- ScopeProvider . ForEachScope ( IterateScopes , builder ) ;
227+ if (
228+ ! _options . DisableAdditionalInformation
229+ && state is IReadOnlyList < KeyValuePair < string , object ? > > additionalInformation
230+ )
231+ {
232+ _ = builder . Append ( '\n ' ) . Append ( '\t ' ) . Append ( "Additional Information" ) ;
233+ foreach ( var info in additionalInformation )
234+ {
235+ AddAdditionalInformation ( builder , info ) ;
236+ }
237+ }
195238
196- return ( builder , scopes ) ;
239+ ScopeProvider . ForEachScope ( IterateScopes , builder ) ;
240+
241+ return ( builder . ToString ( ) , scopes ) ;
242+ }
243+ finally
244+ {
245+ _ = builder . Clear ( ) ;
246+ if ( builder . Capacity > DefaultCapacity )
247+ {
248+ builder . Capacity = DefaultCapacity ;
249+ }
250+ _builder = builder ;
251+ }
197252
198253 void IterateScopes ( object ? scope , StringBuilder state )
199254 {
0 commit comments