Skip to content

Commit 8107aa5

Browse files
authored
Cleanup logs after successful test runs (#40807)
- backport for #39038 - `cherry-pick` of 6958517 - update `AssemblyTestLog` to perform actual log directory cleanup - add `ReportTestFailure()` method for tests to report failures, disabling cleanup - add `IAcceptFailureReports` for `AspNetTestAssemblyRunner` to report failures to `AssemblyTestLog` - extend `AspNetTestAssemblyRunner` to optionally create fixture instances using `static ForAssembly(Assembly)` - add `AssemblyTestLogFixtureAttribute` to register `AssemblyTestLog` as an assembly fixture - use `AssemblyTestLogFixtureAttribute` in all test projects - disable log cleanup in three existing tests - add tests of new cleanup features - also cover a few existing methods - do cleanup before creating new logger - was deleting the just-created global.log file nits: - use `is [not] null` and `new()` more - move `AssemblyTestLogTests` to same namespace as `AssemblyTestLog`
1 parent eb694ab commit 8107aa5

10 files changed

+755
-71
lines changed

src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,25 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.IO;
6-
using System.Linq;
74
using System.Net;
85
using System.Net.Http;
96
using System.Net.Http.Headers;
107
using System.Text.Encodings.Web;
11-
using System.Threading.Tasks;
128
using Microsoft.AspNetCore.Hosting;
139
using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
1410
using Microsoft.AspNetCore.TestHost;
1511
using Microsoft.AspNetCore.Testing;
1612
using Microsoft.Extensions.DependencyInjection;
1713
using Microsoft.Extensions.Logging;
1814
using Microsoft.Extensions.Logging.Testing;
19-
using Xunit;
2015
using Xunit.Abstractions;
2116

2217
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
2318
{
2419
/// <summary>
2520
/// Functional test to verify the error reporting of Razor compilation by diagnostic middleware.
2621
/// </summary>
27-
public class ErrorPageTests : IClassFixture<MvcTestFixture<ErrorPageMiddlewareWebSite.Startup>>, IDisposable
22+
public class ErrorPageTests : IClassFixture<MvcTestFixture<ErrorPageMiddlewareWebSite.Startup>>
2823
{
2924
private static readonly string PreserveCompilationContextMessage = HtmlEncoder.Default.Encode(
3025
"One or more compilation references may be missing. " +
@@ -189,10 +184,5 @@ public async Task AggregateException_FlattensInnerExceptions()
189184
Assert.Contains(nullReferenceException, content);
190185
Assert.Contains(indexOutOfRangeException, content);
191186
}
192-
193-
public void Dispose()
194-
{
195-
_assemblyTestLog.Dispose();
196-
}
197187
}
198188
}

src/Testing/src/AssemblyTestLog.cs

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
using System.Diagnostics.CodeAnalysis;
88
using System.Globalization;
99
using System.IO;
10-
using System.Linq;
1110
using System.Reflection;
1211
using System.Runtime.CompilerServices;
13-
using System.Text;
1412
using Microsoft.Extensions.DependencyInjection;
1513
using Microsoft.Extensions.Logging;
1614
using Serilog;
@@ -22,20 +20,21 @@
2220

2321
namespace Microsoft.AspNetCore.Testing
2422
{
25-
public class AssemblyTestLog : IDisposable
23+
public class AssemblyTestLog : IAcceptFailureReports, IDisposable
2624
{
2725
private const string MaxPathLengthEnvironmentVariableName = "ASPNETCORE_TEST_LOG_MAXPATH";
2826
private const string LogFileExtension = ".log";
2927
private static readonly int MaxPathLength = GetMaxPathLength();
3028

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();
3331

3432
private readonly ILoggerFactory _globalLoggerFactory;
3533
private readonly ILogger _globalLogger;
3634
private readonly string _baseDirectory;
3735
private readonly Assembly _assembly;
3836
private readonly IServiceProvider _serviceProvider;
37+
private bool _testFailureReported;
3938

4039
private static int GetMaxPathLength()
4140
{
@@ -53,6 +52,9 @@ private AssemblyTestLog(ILoggerFactory globalLoggerFactory, ILogger globalLogger
5352
_serviceProvider = serviceProvider;
5453
}
5554

55+
// internal for testing
56+
internal bool OnCI { get; set; } = SkipOnCIAttribute.OnCI();
57+
5658
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
5759
public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) =>
5860
StartTestLog(output, className, out loggerFactory, LogLevel.Debug, testName);
@@ -178,11 +180,8 @@ public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string cl
178180
return serviceCollection.BuildServiceProvider();
179181
}
180182

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)
186185
{
187186
var logStart = DateTimeOffset.UtcNow;
188187
SerilogLoggerProvider serilogLoggerProvider = null;
@@ -224,26 +223,46 @@ public static AssemblyTestLog ForAssembly(Assembly assembly)
224223
{
225224
if (!_logs.TryGetValue(assembly, out var log))
226225
{
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+
}
228236

229-
log = Create(assembly, baseDirectory);
230-
_logs[assembly] = log;
237+
var baseDirectory = TestFileOutputContext.GetOutputDirectory(assembly);
231238

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.
233240
var assemblyBaseDirectory = TestFileOutputContext.GetAssemblyBaseDirectory(assembly);
234-
if (!string.IsNullOrEmpty(assemblyBaseDirectory) && !TestFileOutputContext.GetPreserveExistingLogsInOutput(assembly))
241+
if (!string.IsNullOrEmpty(assemblyBaseDirectory) &&
242+
!TestFileOutputContext.GetPreserveExistingLogsInOutput(assembly))
235243
{
236244
try
237245
{
238246
Directory.Delete(assemblyBaseDirectory, recursive: true);
239247
}
240-
catch { }
248+
catch
249+
{
250+
}
241251
}
252+
253+
log = Create(assembly, baseDirectory);
254+
_logs[assembly] = log;
242255
}
256+
243257
return log;
244258
}
245259
}
246260

261+
public void ReportTestFailure()
262+
{
263+
_testFailureReported = true;
264+
}
265+
247266
private static TestFrameworkFileLoggerAttribute GetFileLoggerAttribute(Assembly assembly)
248267
=> assembly.GetCustomAttribute<TestFrameworkFileLoggerAttribute>()
249268
?? throw new InvalidOperationException($"No {nameof(TestFrameworkFileLoggerAttribute)} found on the assembly {assembly.GetName().Name}. "
@@ -269,13 +288,32 @@ private static SerilogLoggerProvider ConfigureFileLogging(string fileName, DateT
269288
.MinimumLevel.Verbose()
270289
.WriteTo.File(fileName, outputTemplate: "[{TimestampOffset}] [{SourceContext}] [{Level}] {Message:l}{NewLine}{Exception}", flushToDiskInterval: TimeSpan.FromSeconds(1), shared: true)
271290
.CreateLogger();
291+
272292
return new SerilogLoggerProvider(serilogger, dispose: true);
273293
}
274294

275-
public void Dispose()
295+
void IDisposable.Dispose()
276296
{
277297
(_serviceProvider as IDisposable)?.Dispose();
278298
_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+
}
279317
}
280318

281319
private class AssemblyLogTimestampOffsetEnricher : ILogEventEnricher
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Testing;
5+
6+
public class AssemblyTestLogFixtureAttribute : AssemblyFixtureAttribute
7+
{
8+
public AssemblyTestLogFixtureAttribute() : base(typeof(AssemblyTestLog))
9+
{
10+
}
11+
}

src/Testing/src/build/Microsoft.AspNetCore.Testing.props

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
</PropertyGroup>
1212

1313
<Target Name="SetLoggingTestingAssemblyAttributes"
14-
BeforeTargets="GetAssemblyAttributes"
15-
Condition="'$(GenerateLoggingTestingAssemblyAttributes)' != 'false'">
14+
BeforeTargets="GetAssemblyAttributes"
15+
Condition="'$(GenerateLoggingTestingAssemblyAttributes)' != 'false'">
1616
<PropertyGroup>
1717
<PreserveExistingLogsInOutput Condition="'$(PreserveExistingLogsInOutput)' == '' AND '$(ContinuousIntegrationBuild)' == 'true'">true</PreserveExistingLogsInOutput>
1818
<PreserveExistingLogsInOutput Condition="'$(PreserveExistingLogsInOutput)' == ''">false</PreserveExistingLogsInOutput>
@@ -24,6 +24,7 @@
2424
<_Parameter2>Microsoft.AspNetCore.Testing</_Parameter2>
2525
</AssemblyAttribute>
2626

27+
<AssemblyAttribute Include="Microsoft.AspNetCore.Testing.AssemblyTestLogFixtureAttribute" />
2728
<AssemblyAttribute Include="Microsoft.AspNetCore.Testing.TestFrameworkFileLoggerAttribute">
2829
<_Parameter1>$(PreserveExistingLogsInOutput)</_Parameter1>
2930
<_Parameter2>$(TargetFramework)</_Parameter2>

src/Testing/src/xunit/AspNetTestAssemblyRunner.cs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Reflection;
78
using System.Threading;
89
using System.Threading.Tasks;
910
using Xunit;
@@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Testing
1415
{
1516
public class AspNetTestAssemblyRunner : XunitTestAssemblyRunner
1617
{
17-
private readonly Dictionary<Type, object> _assemblyFixtureMappings = new Dictionary<Type, object>();
18+
private readonly Dictionary<Type, object> _assemblyFixtureMappings = new();
1819

1920
public AspNetTestAssemblyRunner(
2021
ITestAssembly testAssembly,
@@ -26,31 +27,49 @@ public AspNetTestAssemblyRunner(
2627
{
2728
}
2829

30+
// internal for testing
31+
internal IEnumerable<object> Fixtures => _assemblyFixtureMappings.Values;
32+
2933
protected override async Task AfterTestAssemblyStartingAsync()
3034
{
3135
await base.AfterTestAssemblyStartingAsync();
3236

3337
// Find all the AssemblyFixtureAttributes on the test assembly
3438
await Aggregator.RunAsync(async () =>
3539
{
36-
var fixturesAttributes = ((IReflectionAssemblyInfo)TestAssembly.Assembly)
37-
.Assembly
40+
var assembly = ((IReflectionAssemblyInfo)TestAssembly.Assembly).Assembly;
41+
var fixturesAttributes = assembly
3842
.GetCustomAttributes(typeof(AssemblyFixtureAttribute), false)
3943
.Cast<AssemblyFixtureAttribute>()
4044
.ToList();
4145

4246
// Instantiate all the fixtures
4347
foreach (var fixtureAttribute in fixturesAttributes)
4448
{
45-
var ctorWithDiagnostics = fixtureAttribute.FixtureType.GetConstructor(new[] { typeof(IMessageSink) });
4649
object instance = null;
47-
if (ctorWithDiagnostics != null)
50+
var staticCreator = fixtureAttribute.FixtureType.GetMethod(
51+
name: "ForAssembly",
52+
bindingAttr: BindingFlags.Public | BindingFlags.Static,
53+
binder: null,
54+
types: new[] { typeof(Assembly) },
55+
modifiers: null);
56+
if (staticCreator is null)
4857
{
49-
instance = Activator.CreateInstance(fixtureAttribute.FixtureType, DiagnosticMessageSink);
58+
var ctorWithDiagnostics = fixtureAttribute
59+
.FixtureType
60+
.GetConstructor(new[] { typeof(IMessageSink) });
61+
if (ctorWithDiagnostics is null)
62+
{
63+
instance = Activator.CreateInstance(fixtureAttribute.FixtureType);
64+
}
65+
else
66+
{
67+
instance = Activator.CreateInstance(fixtureAttribute.FixtureType, DiagnosticMessageSink);
68+
}
5069
}
5170
else
5271
{
53-
instance = Activator.CreateInstance(fixtureAttribute.FixtureType);
72+
instance = staticCreator.Invoke(obj: null, parameters: new[] { assembly });
5473
}
5574

5675
_assemblyFixtureMappings[fixtureAttribute.FixtureType] = instance;
@@ -66,32 +85,44 @@ await Aggregator.RunAsync(async () =>
6685
protected override async Task BeforeTestAssemblyFinishedAsync()
6786
{
6887
// Dispose fixtures
69-
foreach (var disposable in _assemblyFixtureMappings.Values.OfType<IDisposable>())
88+
foreach (var disposable in Fixtures.OfType<IDisposable>())
7089
{
7190
Aggregator.Run(disposable.Dispose);
7291
}
7392

74-
foreach (var disposable in _assemblyFixtureMappings.Values.OfType<IAsyncLifetime>())
93+
foreach (var disposable in Fixtures.OfType<IAsyncLifetime>())
7594
{
7695
await Aggregator.RunAsync(disposable.DisposeAsync);
7796
}
7897

7998
await base.BeforeTestAssemblyFinishedAsync();
8099
}
81100

82-
protected override Task<RunSummary> RunTestCollectionAsync(
101+
protected override async Task<RunSummary> RunTestCollectionAsync(
83102
IMessageBus messageBus,
84103
ITestCollection testCollection,
85104
IEnumerable<IXunitTestCase> testCases,
86105
CancellationTokenSource cancellationTokenSource)
87-
=> new AspNetTestCollectionRunner(
106+
{
107+
var runSummary = await new AspNetTestCollectionRunner(
88108
_assemblyFixtureMappings,
89109
testCollection,
90110
testCases,
91111
DiagnosticMessageSink,
92112
messageBus,
93113
TestCaseOrderer,
94114
new ExceptionAggregator(Aggregator),
95-
cancellationTokenSource).RunAsync();
115+
cancellationTokenSource)
116+
.RunAsync();
117+
if (runSummary.Failed != 0)
118+
{
119+
foreach (var fixture in Fixtures.OfType<IAcceptFailureReports>())
120+
{
121+
fixture.ReportTestFailure();
122+
}
123+
}
124+
125+
return runSummary;
126+
}
96127
}
97128
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Testing;
5+
6+
internal interface IAcceptFailureReports
7+
{
8+
void ReportTestFailure();
9+
}

0 commit comments

Comments
 (0)