diff --git a/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs b/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs
index cc74a6244d72..d09f99af276b 100644
--- a/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs
+++ b/src/Mvc/test/Mvc.FunctionalTests/ErrorPageTests.cs
@@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests;
///
/// Functional test to verify the error reporting of Razor compilation by diagnostic middleware.
///
-public class ErrorPageTests : IClassFixture>, IDisposable
+public class ErrorPageTests : IClassFixture>
{
private static readonly string PreserveCompilationContextMessage = HtmlEncoder.Default.Encode(
"One or more compilation references may be missing. " +
@@ -183,9 +183,4 @@ public async Task AggregateException_FlattensInnerExceptions()
Assert.Contains(nullReferenceException, content);
Assert.Contains(indexOutOfRangeException, content);
}
-
- public void Dispose()
- {
- _assemblyTestLog.Dispose();
- }
}
diff --git a/src/Testing/src/AssemblyTestLog.cs b/src/Testing/src/AssemblyTestLog.cs
index 9ca775cd45ee..027e41f53c62 100644
--- a/src/Testing/src/AssemblyTestLog.cs
+++ b/src/Testing/src/AssemblyTestLog.cs
@@ -20,20 +20,21 @@
namespace Microsoft.AspNetCore.Testing;
-public class AssemblyTestLog : IDisposable
+public class AssemblyTestLog : IAcceptFailureReports, IDisposable
{
private const string MaxPathLengthEnvironmentVariableName = "ASPNETCORE_TEST_LOG_MAXPATH";
private const string LogFileExtension = ".log";
private static readonly int MaxPathLength = GetMaxPathLength();
- private static readonly object _lock = new object();
- private static readonly Dictionary _logs = new Dictionary();
+ private static readonly object _lock = new();
+ private static readonly Dictionary _logs = new();
private readonly ILoggerFactory _globalLoggerFactory;
private readonly ILogger _globalLogger;
private readonly string _baseDirectory;
private readonly Assembly _assembly;
private readonly IServiceProvider _serviceProvider;
+ private bool _testFailureReported;
private static int GetMaxPathLength()
{
@@ -51,6 +52,9 @@ private AssemblyTestLog(ILoggerFactory globalLoggerFactory, ILogger globalLogger
_serviceProvider = serviceProvider;
}
+ // internal for testing
+ internal bool OnCI { get; set; } = SkipOnCIAttribute.OnCI();
+
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) =>
StartTestLog(output, className, out loggerFactory, LogLevel.Debug, testName);
@@ -176,7 +180,8 @@ public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string cl
return serviceCollection.BuildServiceProvider();
}
- public static AssemblyTestLog Create(Assembly assembly, string baseDirectory)
+ // internal for testing. Expectation is AspNetTestAssembly runner calls ForAssembly() first for every Assembly.
+ internal static AssemblyTestLog Create(Assembly assembly, string baseDirectory)
{
var logStart = DateTimeOffset.UtcNow;
SerilogLoggerProvider serilogLoggerProvider = null;
@@ -218,12 +223,20 @@ public static AssemblyTestLog ForAssembly(Assembly assembly)
{
if (!_logs.TryGetValue(assembly, out var log))
{
- var baseDirectory = TestFileOutputContext.GetOutputDirectory(assembly);
+ var stackTrace = Environment.StackTrace;
+ if (!stackTrace.Contains(
+ "Microsoft.AspNetCore.Testing"
+#if NETCOREAPP
+ , StringComparison.Ordinal
+#endif
+ ))
+ {
+ throw new InvalidOperationException($"Unexpected initial {nameof(ForAssembly)} caller.");
+ }
- log = Create(assembly, baseDirectory);
- _logs[assembly] = log;
+ var baseDirectory = TestFileOutputContext.GetOutputDirectory(assembly);
- // Try to clear previous logs, continue if it fails.
+ // Try to clear previous logs, continue if it fails. Do this before creating new global logger.
var assemblyBaseDirectory = TestFileOutputContext.GetAssemblyBaseDirectory(assembly);
if (!string.IsNullOrEmpty(assemblyBaseDirectory) &&
!TestFileOutputContext.GetPreserveExistingLogsInOutput(assembly))
@@ -232,13 +245,24 @@ public static AssemblyTestLog ForAssembly(Assembly assembly)
{
Directory.Delete(assemblyBaseDirectory, recursive: true);
}
- catch { }
+ catch
+ {
+ }
}
+
+ log = Create(assembly, baseDirectory);
+ _logs[assembly] = log;
}
+
return log;
}
}
+ public void ReportTestFailure()
+ {
+ _testFailureReported = true;
+ }
+
private static TestFrameworkFileLoggerAttribute GetFileLoggerAttribute(Assembly assembly)
=> assembly.GetCustomAttribute()
?? throw new InvalidOperationException($"No {nameof(TestFrameworkFileLoggerAttribute)} found on the assembly {assembly.GetName().Name}. "
@@ -268,10 +292,28 @@ private static SerilogLoggerProvider ConfigureFileLogging(string fileName, DateT
return new SerilogLoggerProvider(serilogger, dispose: true);
}
- public void Dispose()
+ void IDisposable.Dispose()
{
(_serviceProvider as IDisposable)?.Dispose();
_globalLoggerFactory.Dispose();
+
+ // Clean up if no tests failed and we're not running local tests. (Ignoring tests of this class, OnCI is
+ // true on both build and Helix agents.) In particular, remove the directory containing the global.log
+ // file. All test class log files for this assembly are in subdirectories of this location.
+ if (!_testFailureReported &&
+ OnCI &&
+ _baseDirectory is not null &&
+ Directory.Exists(_baseDirectory))
+ {
+ try
+ {
+ Directory.Delete(_baseDirectory, recursive: true);
+ }
+ catch
+ {
+ // Best effort. Ignore problems deleting locked logged files.
+ }
+ }
}
private class AssemblyLogTimestampOffsetEnricher : ILogEventEnricher
diff --git a/src/Testing/src/AssemblyTestLogFixtureAttribute.cs b/src/Testing/src/AssemblyTestLogFixtureAttribute.cs
new file mode 100644
index 000000000000..e4a4452cd458
--- /dev/null
+++ b/src/Testing/src/AssemblyTestLogFixtureAttribute.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Testing;
+
+public class AssemblyTestLogFixtureAttribute : AssemblyFixtureAttribute
+{
+ public AssemblyTestLogFixtureAttribute() : base(typeof(AssemblyTestLog))
+ {
+ }
+}
diff --git a/src/Testing/src/build/Microsoft.AspNetCore.Testing.props b/src/Testing/src/build/Microsoft.AspNetCore.Testing.props
index 063e9094d172..47d06dfef7a7 100644
--- a/src/Testing/src/build/Microsoft.AspNetCore.Testing.props
+++ b/src/Testing/src/build/Microsoft.AspNetCore.Testing.props
@@ -11,8 +11,8 @@
+ BeforeTargets="GetAssemblyAttributes"
+ Condition="'$(GenerateLoggingTestingAssemblyAttributes)' != 'false'">
true
false
@@ -24,6 +24,7 @@
<_Parameter2>Microsoft.AspNetCore.Testing
+
<_Parameter1>$(PreserveExistingLogsInOutput)
<_Parameter2>$(TargetFramework)
diff --git a/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs b/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs
index 0d99ffcff58b..5d149fd48a93 100644
--- a/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs
+++ b/src/Testing/src/xunit/AspNetTestAssemblyRunner.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
@@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Testing;
public class AspNetTestAssemblyRunner : XunitTestAssemblyRunner
{
- private readonly Dictionary _assemblyFixtureMappings = new Dictionary();
+ private readonly Dictionary _assemblyFixtureMappings = new();
public AspNetTestAssemblyRunner(
ITestAssembly testAssembly,
@@ -26,6 +27,9 @@ public AspNetTestAssemblyRunner(
{
}
+ // internal for testing
+ internal IEnumerable