7
7
using System . Diagnostics . CodeAnalysis ;
8
8
using System . Globalization ;
9
9
using System . IO ;
10
- using System . Linq ;
11
10
using System . Reflection ;
12
11
using System . Runtime . CompilerServices ;
13
- using System . Text ;
14
12
using Microsoft . Extensions . DependencyInjection ;
15
13
using Microsoft . Extensions . Logging ;
16
14
using Serilog ;
22
20
23
21
namespace Microsoft . AspNetCore . Testing
24
22
{
25
- public class AssemblyTestLog : IDisposable
23
+ public class AssemblyTestLog : IAcceptFailureReports , IDisposable
26
24
{
27
25
private const string MaxPathLengthEnvironmentVariableName = "ASPNETCORE_TEST_LOG_MAXPATH" ;
28
26
private const string LogFileExtension = ".log" ;
29
27
private static readonly int MaxPathLength = GetMaxPathLength ( ) ;
30
28
31
- private static readonly object _lock = new object ( ) ;
32
- private static readonly Dictionary < Assembly , AssemblyTestLog > _logs = new Dictionary < Assembly , AssemblyTestLog > ( ) ;
29
+ private static readonly object _lock = new ( ) ;
30
+ private static readonly Dictionary < Assembly , AssemblyTestLog > _logs = new ( ) ;
33
31
34
32
private readonly ILoggerFactory _globalLoggerFactory ;
35
33
private readonly ILogger _globalLogger ;
36
34
private readonly string _baseDirectory ;
37
35
private readonly Assembly _assembly ;
38
36
private readonly IServiceProvider _serviceProvider ;
37
+ private bool _testFailureReported ;
39
38
40
39
private static int GetMaxPathLength ( )
41
40
{
@@ -53,6 +52,9 @@ private AssemblyTestLog(ILoggerFactory globalLoggerFactory, ILogger globalLogger
53
52
_serviceProvider = serviceProvider ;
54
53
}
55
54
55
+ // internal for testing
56
+ internal bool OnCI { get ; set ; } = SkipOnCIAttribute . OnCI ( ) ;
57
+
56
58
[ SuppressMessage ( "ApiDesign" , "RS0026:Do not add multiple public overloads with optional parameters" , Justification = "Required to maintain compatibility" ) ]
57
59
public IDisposable StartTestLog ( ITestOutputHelper output , string className , out ILoggerFactory loggerFactory , [ CallerMemberName ] string testName = null ) =>
58
60
StartTestLog ( output , className , out loggerFactory , LogLevel . Debug , testName ) ;
@@ -178,11 +180,8 @@ public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string cl
178
180
return serviceCollection . BuildServiceProvider ( ) ;
179
181
}
180
182
181
- // For back compat
182
- public static AssemblyTestLog Create ( string assemblyName , string baseDirectory )
183
- => Create ( Assembly . Load ( new AssemblyName ( assemblyName ) ) , baseDirectory ) ;
184
-
185
- public static AssemblyTestLog Create ( Assembly assembly , string baseDirectory )
183
+ // internal for testing. Expectation is AspNetTestAssembly runner calls ForAssembly() first for every Assembly.
184
+ internal static AssemblyTestLog Create ( Assembly assembly , string baseDirectory )
186
185
{
187
186
var logStart = DateTimeOffset . UtcNow ;
188
187
SerilogLoggerProvider serilogLoggerProvider = null ;
@@ -224,26 +223,46 @@ public static AssemblyTestLog ForAssembly(Assembly assembly)
224
223
{
225
224
if ( ! _logs . TryGetValue ( assembly , out var log ) )
226
225
{
227
- var baseDirectory = TestFileOutputContext . GetOutputDirectory ( assembly ) ;
226
+ var stackTrace = Environment . StackTrace ;
227
+ if ( ! stackTrace . Contains (
228
+ "Microsoft.AspNetCore.Testing"
229
+ #if NETCOREAPP
230
+ , StringComparison . Ordinal
231
+ #endif
232
+ ) )
233
+ {
234
+ throw new InvalidOperationException ( $ "Unexpected initial { nameof ( ForAssembly ) } caller.") ;
235
+ }
228
236
229
- log = Create ( assembly , baseDirectory ) ;
230
- _logs [ assembly ] = log ;
237
+ var baseDirectory = TestFileOutputContext . GetOutputDirectory ( assembly ) ;
231
238
232
- // Try to clear previous logs, continue if it fails.
239
+ // Try to clear previous logs, continue if it fails. Do this before creating new global logger.
233
240
var assemblyBaseDirectory = TestFileOutputContext . GetAssemblyBaseDirectory ( assembly ) ;
234
- if ( ! string . IsNullOrEmpty ( assemblyBaseDirectory ) && ! TestFileOutputContext . GetPreserveExistingLogsInOutput ( assembly ) )
241
+ if ( ! string . IsNullOrEmpty ( assemblyBaseDirectory ) &&
242
+ ! TestFileOutputContext . GetPreserveExistingLogsInOutput ( assembly ) )
235
243
{
236
244
try
237
245
{
238
246
Directory . Delete ( assemblyBaseDirectory , recursive : true ) ;
239
247
}
240
- catch { }
248
+ catch
249
+ {
250
+ }
241
251
}
252
+
253
+ log = Create ( assembly , baseDirectory ) ;
254
+ _logs [ assembly ] = log ;
242
255
}
256
+
243
257
return log ;
244
258
}
245
259
}
246
260
261
+ public void ReportTestFailure ( )
262
+ {
263
+ _testFailureReported = true ;
264
+ }
265
+
247
266
private static TestFrameworkFileLoggerAttribute GetFileLoggerAttribute ( Assembly assembly )
248
267
=> assembly . GetCustomAttribute < TestFrameworkFileLoggerAttribute > ( )
249
268
?? throw new InvalidOperationException ( $ "No { nameof ( TestFrameworkFileLoggerAttribute ) } found on the assembly { assembly . GetName ( ) . Name } . "
@@ -269,13 +288,32 @@ private static SerilogLoggerProvider ConfigureFileLogging(string fileName, DateT
269
288
. MinimumLevel . Verbose ( )
270
289
. WriteTo . File ( fileName , outputTemplate : "[{TimestampOffset}] [{SourceContext}] [{Level}] {Message:l}{NewLine}{Exception}" , flushToDiskInterval : TimeSpan . FromSeconds ( 1 ) , shared : true )
271
290
. CreateLogger ( ) ;
291
+
272
292
return new SerilogLoggerProvider ( serilogger , dispose : true ) ;
273
293
}
274
294
275
- public void Dispose ( )
295
+ void IDisposable . Dispose ( )
276
296
{
277
297
( _serviceProvider as IDisposable ) ? . Dispose ( ) ;
278
298
_globalLoggerFactory . Dispose ( ) ;
299
+
300
+ // Clean up if no tests failed and we're not running local tests. (Ignoring tests of this class, OnCI is
301
+ // true on both build and Helix agents.) In particular, remove the directory containing the global.log
302
+ // file. All test class log files for this assembly are in subdirectories of this location.
303
+ if ( ! _testFailureReported &&
304
+ OnCI &&
305
+ _baseDirectory is not null &&
306
+ Directory . Exists ( _baseDirectory ) )
307
+ {
308
+ try
309
+ {
310
+ Directory . Delete ( _baseDirectory , recursive : true ) ;
311
+ }
312
+ catch
313
+ {
314
+ // Best effort. Ignore problems deleting locked logged files.
315
+ }
316
+ }
279
317
}
280
318
281
319
private class AssemblyLogTimestampOffsetEnricher : ILogEventEnricher
0 commit comments