Reorganize source code in preparation to move into aspnet/Extensions
Prior to reorganization, this source code was found in 8270c54522
This commit is contained in:
commit
a8c8ddbb45
|
|
@ -0,0 +1,305 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Extensions.Logging;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Logging.Testing
|
||||||
|
{
|
||||||
|
public class AssemblyTestLog : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly string OutputDirectoryEnvironmentVariableName = "ASPNETCORE_TEST_LOG_DIR";
|
||||||
|
private static readonly string MaxPathLengthEnvironmentVariableName = "ASPNETCORE_TEST_LOG_MAXPATH";
|
||||||
|
private static readonly string LogFileExtension = ".log";
|
||||||
|
private static readonly int MaxPathLength = GetMaxPathLength();
|
||||||
|
private static char[] InvalidFileChars = new char[]
|
||||||
|
{
|
||||||
|
'\"', '<', '>', '|', '\0',
|
||||||
|
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
|
||||||
|
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
|
||||||
|
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
|
||||||
|
(char)31, ':', '*', '?', '\\', '/', ' ', (char)127
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly object _lock = new object();
|
||||||
|
private static readonly Dictionary<Assembly, AssemblyTestLog> _logs = new Dictionary<Assembly, AssemblyTestLog>();
|
||||||
|
|
||||||
|
private readonly ILoggerFactory _globalLoggerFactory;
|
||||||
|
private readonly ILogger _globalLogger;
|
||||||
|
private readonly string _baseDirectory;
|
||||||
|
private readonly string _assemblyName;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
private static int GetMaxPathLength()
|
||||||
|
{
|
||||||
|
var maxPathString = Environment.GetEnvironmentVariable(MaxPathLengthEnvironmentVariableName);
|
||||||
|
var defaultMaxPath = 245;
|
||||||
|
return string.IsNullOrEmpty(maxPathString) ? defaultMaxPath : int.Parse(maxPathString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssemblyTestLog(ILoggerFactory globalLoggerFactory, ILogger globalLogger, string baseDirectory, string assemblyName, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_globalLoggerFactory = globalLoggerFactory;
|
||||||
|
_globalLogger = globalLogger;
|
||||||
|
_baseDirectory = baseDirectory;
|
||||||
|
_assemblyName = assemblyName;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) =>
|
||||||
|
StartTestLog(output, className, out loggerFactory, LogLevel.Debug, testName);
|
||||||
|
|
||||||
|
public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, LogLevel minLogLevel, [CallerMemberName] string testName = null) =>
|
||||||
|
StartTestLog(output, className, out loggerFactory, minLogLevel, out var _, testName);
|
||||||
|
|
||||||
|
internal IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, LogLevel minLogLevel, out string resolvedTestName, [CallerMemberName] string testName = null)
|
||||||
|
{
|
||||||
|
var serviceProvider = CreateLoggerServices(output, className, minLogLevel, out resolvedTestName, testName);
|
||||||
|
var factory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||||
|
loggerFactory = factory;
|
||||||
|
var logger = loggerFactory.CreateLogger("TestLifetime");
|
||||||
|
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
var scope = logger.BeginScope("Test: {testName}", testName);
|
||||||
|
|
||||||
|
_globalLogger.LogInformation("Starting test {testName}", testName);
|
||||||
|
logger.LogInformation("Starting test {testName}", testName);
|
||||||
|
|
||||||
|
return new Disposable(() =>
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
_globalLogger.LogInformation("Finished test {testName} in {duration}s", testName, stopwatch.Elapsed.TotalSeconds);
|
||||||
|
logger.LogInformation("Finished test {testName} in {duration}s", testName, stopwatch.Elapsed.TotalSeconds);
|
||||||
|
scope.Dispose();
|
||||||
|
factory.Dispose();
|
||||||
|
(serviceProvider as IDisposable)?.Dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILoggerFactory CreateLoggerFactory(ITestOutputHelper output, string className, [CallerMemberName] string testName = null) =>
|
||||||
|
CreateLoggerFactory(output, className, LogLevel.Trace, testName);
|
||||||
|
|
||||||
|
public ILoggerFactory CreateLoggerFactory(ITestOutputHelper output, string className, LogLevel minLogLevel, [CallerMemberName] string testName = null)
|
||||||
|
{
|
||||||
|
return CreateLoggerServices(output, className, minLogLevel, out var _, testName).GetRequiredService<ILoggerFactory>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string className, LogLevel minLogLevel, out string normalizedTestName, [CallerMemberName] string testName = null)
|
||||||
|
{
|
||||||
|
normalizedTestName = string.Empty;
|
||||||
|
|
||||||
|
// Try to shorten the class name using the assembly name
|
||||||
|
if (className.StartsWith(_assemblyName + "."))
|
||||||
|
{
|
||||||
|
className = className.Substring(_assemblyName.Length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
SerilogLoggerProvider serilogLoggerProvider = null;
|
||||||
|
if (!string.IsNullOrEmpty(_baseDirectory))
|
||||||
|
{
|
||||||
|
var testOutputDirectory = Path.Combine(GetAssemblyBaseDirectory(_assemblyName, _baseDirectory), className);
|
||||||
|
testName = RemoveIllegalFileChars(testName);
|
||||||
|
|
||||||
|
if (testOutputDirectory.Length + testName.Length + LogFileExtension.Length >= MaxPathLength)
|
||||||
|
{
|
||||||
|
_globalLogger.LogWarning($"Test name {testName} is too long. Please shorten test name.");
|
||||||
|
|
||||||
|
// Shorten the test name by removing the middle portion of the testname
|
||||||
|
var testNameLength = MaxPathLength - testOutputDirectory.Length - LogFileExtension.Length;
|
||||||
|
|
||||||
|
if (testNameLength <= 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Output file path could not be constructed due to max path length restrictions. Please shorten test assembly, class or method names.");
|
||||||
|
}
|
||||||
|
|
||||||
|
testName = testName.Substring(0, testNameLength / 2) + testName.Substring(testName.Length - testNameLength / 2, testNameLength / 2);
|
||||||
|
|
||||||
|
_globalLogger.LogWarning($"To prevent long paths test name was shortened to {testName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var testOutputFile = Path.Combine(testOutputDirectory, $"{testName}{LogFileExtension}");
|
||||||
|
|
||||||
|
if (File.Exists(testOutputFile))
|
||||||
|
{
|
||||||
|
_globalLogger.LogWarning($"Output log file {testOutputFile} already exists. Please try to keep log file names unique.");
|
||||||
|
|
||||||
|
for (var i = 0; i < 1000; i++)
|
||||||
|
{
|
||||||
|
testOutputFile = Path.Combine(testOutputDirectory, $"{testName}.{i}{LogFileExtension}");
|
||||||
|
|
||||||
|
if (!File.Exists(testOutputFile))
|
||||||
|
{
|
||||||
|
_globalLogger.LogWarning($"To resolve log file collision, the enumerated file {testOutputFile} will be used.");
|
||||||
|
testName = $"{testName}.{i}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedTestName = testName;
|
||||||
|
serilogLoggerProvider = ConfigureFileLogging(testOutputFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceCollection = new ServiceCollection();
|
||||||
|
serviceCollection.AddLogging(builder =>
|
||||||
|
{
|
||||||
|
builder.SetMinimumLevel(minLogLevel);
|
||||||
|
|
||||||
|
if (output != null)
|
||||||
|
{
|
||||||
|
builder.AddXunit(output, minLogLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serilogLoggerProvider != null)
|
||||||
|
{
|
||||||
|
// Use a factory so that the container will dispose it
|
||||||
|
builder.Services.AddSingleton<ILoggerProvider>(_ => serilogLoggerProvider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return serviceCollection.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AssemblyTestLog Create(string assemblyName, string baseDirectory)
|
||||||
|
{
|
||||||
|
SerilogLoggerProvider serilogLoggerProvider = null;
|
||||||
|
var globalLogDirectory = GetAssemblyBaseDirectory(assemblyName, baseDirectory);
|
||||||
|
if (!string.IsNullOrEmpty(globalLogDirectory))
|
||||||
|
{
|
||||||
|
var globalLogFileName = Path.Combine(globalLogDirectory, "global.log");
|
||||||
|
serilogLoggerProvider = ConfigureFileLogging(globalLogFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceCollection = new ServiceCollection();
|
||||||
|
|
||||||
|
serviceCollection.AddLogging(builder =>
|
||||||
|
{
|
||||||
|
// Global logging, when it's written, is expected to be outputted. So set the log level to minimum.
|
||||||
|
builder.SetMinimumLevel(LogLevel.Trace);
|
||||||
|
|
||||||
|
if (serilogLoggerProvider != null)
|
||||||
|
{
|
||||||
|
// Use a factory so that the container will dispose it
|
||||||
|
builder.Services.AddSingleton<ILoggerProvider>(_ => serilogLoggerProvider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||||
|
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||||
|
|
||||||
|
var logger = loggerFactory.CreateLogger("GlobalTestLog");
|
||||||
|
logger.LogInformation($"Global Test Logging initialized. Set the '{OutputDirectoryEnvironmentVariableName}' Environment Variable in order to create log files on disk.");
|
||||||
|
return new AssemblyTestLog(loggerFactory, logger, baseDirectory, assemblyName, serviceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AssemblyTestLog ForAssembly(Assembly assembly)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!_logs.TryGetValue(assembly, out var log))
|
||||||
|
{
|
||||||
|
var assemblyName = assembly.GetName().Name;
|
||||||
|
var baseDirectory = Environment.GetEnvironmentVariable(OutputDirectoryEnvironmentVariableName);
|
||||||
|
log = Create(assemblyName, baseDirectory);
|
||||||
|
_logs[assembly] = log;
|
||||||
|
|
||||||
|
// Try to clear previous logs
|
||||||
|
var assemblyBaseDirectory = GetAssemblyBaseDirectory(assemblyName, baseDirectory);
|
||||||
|
if (Directory.Exists(assemblyBaseDirectory))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(assemblyBaseDirectory, recursive: true);
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAssemblyBaseDirectory(string assemblyName, string baseDirectory)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(baseDirectory))
|
||||||
|
{
|
||||||
|
return Path.Combine(baseDirectory, assemblyName, RuntimeInformation.FrameworkDescription.TrimStart('.'));
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SerilogLoggerProvider ConfigureFileLogging(string fileName)
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(fileName);
|
||||||
|
if (!Directory.Exists(dir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(fileName))
|
||||||
|
{
|
||||||
|
File.Delete(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var serilogger = new LoggerConfiguration()
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.MinimumLevel.Verbose()
|
||||||
|
.WriteTo.File(fileName, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{SourceContext}] [{Level}] {Message}{NewLine}{Exception}", flushToDiskInterval: TimeSpan.FromSeconds(1), shared: true)
|
||||||
|
.CreateLogger();
|
||||||
|
return new SerilogLoggerProvider(serilogger, dispose: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveIllegalFileChars(string s)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var c in s)
|
||||||
|
{
|
||||||
|
if (InvalidFileChars.Contains(c))
|
||||||
|
{
|
||||||
|
if (sb.Length > 0 && sb[sb.Length - 1] != '_')
|
||||||
|
{
|
||||||
|
sb.Append('_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
(_serviceProvider as IDisposable)?.Dispose();
|
||||||
|
_globalLoggerFactory.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Disposable : IDisposable
|
||||||
|
{
|
||||||
|
private Action _action;
|
||||||
|
|
||||||
|
public Disposable(Action action)
|
||||||
|
{
|
||||||
|
_action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<Project>
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="Xunit.TestFramework">
|
||||||
|
<_Parameter1>Microsoft.Extensions.Logging.Testing.LoggedTestFramework</_Parameter1>
|
||||||
|
<_Parameter2>Microsoft.Extensions.Logging.Testing</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Logging.Testing.Tests
|
||||||
|
{
|
||||||
|
public class AssemblyTestLogTests : LoggedTest
|
||||||
|
{
|
||||||
|
private static readonly Assembly ThisAssembly = typeof(AssemblyTestLog).GetTypeInfo().Assembly;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FullClassNameUsedWhenShortClassNameAttributeNotSpecified()
|
||||||
|
{
|
||||||
|
Assert.Equal(GetType().FullName, ResolvedTestClassName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ForAssembly_ReturnsSameInstanceForSameAssembly()
|
||||||
|
{
|
||||||
|
Assert.Same(
|
||||||
|
AssemblyTestLog.ForAssembly(ThisAssembly),
|
||||||
|
AssemblyTestLog.ForAssembly(ThisAssembly));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TestLogWritesToITestOutputHelper()
|
||||||
|
{
|
||||||
|
var output = new TestTestOutputHelper();
|
||||||
|
var assemblyLog = AssemblyTestLog.Create("NonExistant.Test.Assembly", baseDirectory: null);
|
||||||
|
|
||||||
|
using (assemblyLog.StartTestLog(output, "NonExistant.Test.Class", out var loggerFactory))
|
||||||
|
{
|
||||||
|
var logger = loggerFactory.CreateLogger("TestLogger");
|
||||||
|
logger.LogInformation("Information!");
|
||||||
|
|
||||||
|
// Trace is disabled by default
|
||||||
|
logger.LogTrace("Trace!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(@"[TIMESTAMP] TestLifetime Information: Starting test TestLogWritesToITestOutputHelper
|
||||||
|
[TIMESTAMP] TestLogger Information: Information!
|
||||||
|
[TIMESTAMP] TestLifetime Information: Finished test TestLogWritesToITestOutputHelper in DURATION
|
||||||
|
", MakeConsistent(output.Output), ignoreLineEndingDifferences: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
private Task TestLogEscapesIllegalFileNames() =>
|
||||||
|
RunTestLogFunctionalTest((tempDir) =>
|
||||||
|
{
|
||||||
|
var illegalTestName = "Testing-https://localhost:5000";
|
||||||
|
var escapedTestName = "Testing-https_localhost_5000";
|
||||||
|
using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", baseDirectory: tempDir))
|
||||||
|
using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, resolvedTestName: out var resolvedTestname, testName: illegalTestName))
|
||||||
|
{
|
||||||
|
Assert.Equal(escapedTestName, resolvedTestname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task TestLogWritesToGlobalLogFile() =>
|
||||||
|
RunTestLogFunctionalTest((tempDir) =>
|
||||||
|
{
|
||||||
|
// Because this test writes to a file, it is a functional test and should be logged
|
||||||
|
// but it's also testing the test logging facility. So this is pretty meta ;)
|
||||||
|
var logger = LoggerFactory.CreateLogger("Test");
|
||||||
|
|
||||||
|
using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir))
|
||||||
|
{
|
||||||
|
logger.LogInformation("Created test log in {baseDirectory}", tempDir);
|
||||||
|
|
||||||
|
using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName"))
|
||||||
|
{
|
||||||
|
var testLogger = testLoggerFactory.CreateLogger("TestLogger");
|
||||||
|
testLogger.LogInformation("Information!");
|
||||||
|
testLogger.LogTrace("Trace!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Finished test log in {baseDirectory}", tempDir);
|
||||||
|
|
||||||
|
var globalLogPath = Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "global.log");
|
||||||
|
var testLog = Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.log");
|
||||||
|
|
||||||
|
Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist");
|
||||||
|
Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist");
|
||||||
|
|
||||||
|
var globalLogContent = MakeConsistent(File.ReadAllText(globalLogPath));
|
||||||
|
var testLogContent = MakeConsistent(File.ReadAllText(testLog));
|
||||||
|
|
||||||
|
Assert.Equal(@"[GlobalTestLog] [Information] Global Test Logging initialized. Set the 'ASPNETCORE_TEST_LOG_DIR' Environment Variable in order to create log files on disk.
|
||||||
|
[GlobalTestLog] [Information] Starting test ""FakeTestName""
|
||||||
|
[GlobalTestLog] [Information] Finished test ""FakeTestName"" in DURATION
|
||||||
|
", globalLogContent, ignoreLineEndingDifferences: true);
|
||||||
|
Assert.Equal(@"[TestLifetime] [Information] Starting test ""FakeTestName""
|
||||||
|
[TestLogger] [Information] Information!
|
||||||
|
[TestLogger] [Verbose] Trace!
|
||||||
|
[TestLifetime] [Information] Finished test ""FakeTestName"" in DURATION
|
||||||
|
", testLogContent, ignoreLineEndingDifferences: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task TestLogTruncatesTestNameToAvoidLongPaths() =>
|
||||||
|
RunTestLogFunctionalTest((tempDir) =>
|
||||||
|
{
|
||||||
|
var longTestName = new string('0', 50) + new string('1', 50) + new string('2', 50) + new string('3', 50) + new string('4', 50);
|
||||||
|
var logger = LoggerFactory.CreateLogger("Test");
|
||||||
|
using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir))
|
||||||
|
{
|
||||||
|
logger.LogInformation("Created test log in {baseDirectory}", tempDir);
|
||||||
|
|
||||||
|
using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: longTestName))
|
||||||
|
{
|
||||||
|
testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.LogInformation("Finished test log in {baseDirectory}", tempDir);
|
||||||
|
|
||||||
|
var testLogFiles = new DirectoryInfo(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass")).EnumerateFiles();
|
||||||
|
var testLog = Assert.Single(testLogFiles);
|
||||||
|
var testFileName = Path.GetFileNameWithoutExtension(testLog.Name);
|
||||||
|
|
||||||
|
// The first half of the file comes from the beginning of the test name passed to the logger
|
||||||
|
Assert.Equal(longTestName.Substring(0, testFileName.Length / 2), testFileName.Substring(0, testFileName.Length / 2));
|
||||||
|
// The last half of the file comes from the ending of the test name passed to the logger
|
||||||
|
Assert.Equal(longTestName.Substring(longTestName.Length - testFileName.Length / 2, testFileName.Length / 2), testFileName.Substring(testFileName.Length - testFileName.Length / 2, testFileName.Length / 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task TestLogEnumerateFilenamesToAvoidCollisions() =>
|
||||||
|
RunTestLogFunctionalTest((tempDir) =>
|
||||||
|
{
|
||||||
|
var logger = LoggerFactory.CreateLogger("Test");
|
||||||
|
using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir))
|
||||||
|
{
|
||||||
|
logger.LogInformation("Created test log in {baseDirectory}", tempDir);
|
||||||
|
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName"))
|
||||||
|
{
|
||||||
|
testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.LogInformation("Finished test log in {baseDirectory}", tempDir);
|
||||||
|
|
||||||
|
// The first log file exists
|
||||||
|
Assert.True(File.Exists(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.log")));
|
||||||
|
|
||||||
|
// Subsequent files exist
|
||||||
|
for (var i = 0; i < 9; i++)
|
||||||
|
{
|
||||||
|
Assert.True(File.Exists(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.{i}.log")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private static readonly Regex TimestampRegex = new Regex(@"\d+-\d+-\d+T\d+:\d+:\d+");
|
||||||
|
private static readonly Regex DurationRegex = new Regex(@"[^ ]+s$");
|
||||||
|
|
||||||
|
private async Task RunTestLogFunctionalTest(Action<string> action, [CallerMemberName] string testName = null)
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), $"TestLogging_{Guid.NewGuid().ToString("N")}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action(tempDir);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(tempDir))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, recursive: true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
Directory.Delete(tempDir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string MakeConsistent(string input)
|
||||||
|
{
|
||||||
|
return string.Join(Environment.NewLine, input.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
|
||||||
|
.Select(line =>
|
||||||
|
{
|
||||||
|
var strippedPrefix = line.IndexOf("[") >= 0 ? line.Substring(line.IndexOf("[")) : line;
|
||||||
|
|
||||||
|
var strippedDuration =
|
||||||
|
DurationRegex.Replace(strippedPrefix, "DURATION");
|
||||||
|
var strippedTimestamp = TimestampRegex.Replace(strippedDuration, "TIMESTAMP");
|
||||||
|
return strippedTimestamp;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Logging.Testing.Tests
|
||||||
|
{
|
||||||
|
[ShortClassName]
|
||||||
|
public class LoggedTestXunitTests : TestLoggedTest
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public LoggedTestXunitTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortClassNameUsedWhenShortClassNameAttributeSpecified()
|
||||||
|
{
|
||||||
|
Assert.Equal(GetType().Name, ResolvedTestClassName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoggedTestTestOutputHelperSameInstanceAsInjectedConstructorArg()
|
||||||
|
{
|
||||||
|
Assert.Same(_output, TestOutputHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoggedFactInitializesLoggedTestProperties()
|
||||||
|
{
|
||||||
|
Assert.NotNull(Logger);
|
||||||
|
Assert.NotNull(LoggerFactory);
|
||||||
|
Assert.NotNull(TestSink);
|
||||||
|
Assert.NotNull(TestOutputHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Hello world")]
|
||||||
|
public void LoggedTheoryInitializesLoggedTestProperties(string argument)
|
||||||
|
{
|
||||||
|
Assert.NotNull(Logger);
|
||||||
|
Assert.NotNull(LoggerFactory);
|
||||||
|
Assert.NotNull(TestSink);
|
||||||
|
Assert.NotNull(TestOutputHelper);
|
||||||
|
// Use the test argument
|
||||||
|
Assert.NotNull(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
public void ConditionalLoggedFactGetsInitializedLoggerFactory()
|
||||||
|
{
|
||||||
|
Assert.NotNull(Logger);
|
||||||
|
Assert.NotNull(LoggerFactory);
|
||||||
|
Assert.NotNull(TestSink);
|
||||||
|
Assert.NotNull(TestOutputHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalTheory]
|
||||||
|
[InlineData("Hello world")]
|
||||||
|
public void LoggedConditionalTheoryInitializesLoggedTestProperties(string argument)
|
||||||
|
{
|
||||||
|
Assert.NotNull(Logger);
|
||||||
|
Assert.NotNull(LoggerFactory);
|
||||||
|
Assert.NotNull(TestSink);
|
||||||
|
Assert.NotNull(TestOutputHelper);
|
||||||
|
// Use the test argument
|
||||||
|
Assert.NotNull(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[LogLevel(LogLevel.Information)]
|
||||||
|
public void LoggedFactFilteredByLogLevel()
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Information");
|
||||||
|
Logger.LogDebug("Debug");
|
||||||
|
|
||||||
|
var message = Assert.Single(TestSink.Writes);
|
||||||
|
Assert.Equal(LogLevel.Information, message.LogLevel);
|
||||||
|
Assert.Equal("Information", message.Formatter(message.State, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Hello world")]
|
||||||
|
[LogLevel(LogLevel.Information)]
|
||||||
|
public void LoggedTheoryFilteredByLogLevel(string argument)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Information");
|
||||||
|
Logger.LogDebug("Debug");
|
||||||
|
|
||||||
|
var message = Assert.Single(TestSink.Writes);
|
||||||
|
Assert.Equal(LogLevel.Information, message.LogLevel);
|
||||||
|
Assert.Equal("Information", message.Formatter(message.State, null));
|
||||||
|
|
||||||
|
// Use the test argument
|
||||||
|
Assert.NotNull(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddTestLoggingUpdatedWhenLoggerFactoryIsSet()
|
||||||
|
{
|
||||||
|
var loggerFactory = new LoggerFactory();
|
||||||
|
var serviceCollection = new ServiceCollection();
|
||||||
|
|
||||||
|
LoggerFactory = loggerFactory;
|
||||||
|
AddTestLogging(serviceCollection);
|
||||||
|
|
||||||
|
Assert.Same(loggerFactory, serviceCollection.BuildServiceProvider().GetRequiredService<ILoggerFactory>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalTheory]
|
||||||
|
[EnvironmentVariableSkipCondition("ASPNETCORE_TEST_LOG_DIR", "")] // The test name is only generated when logging is enabled via the environment variable
|
||||||
|
[InlineData(null)]
|
||||||
|
public void LoggedTheoryNullArgumentsAreEscaped(string argument)
|
||||||
|
{
|
||||||
|
Assert.NotNull(LoggerFactory);
|
||||||
|
Assert.Equal($"{nameof(LoggedTheoryNullArgumentsAreEscaped)}_null", ResolvedTestMethodName);
|
||||||
|
// Use the test argument
|
||||||
|
Assert.Null(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AdditionalSetupInvoked()
|
||||||
|
{
|
||||||
|
Assert.True(SetupInvoked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestLoggedTest : LoggedTest
|
||||||
|
{
|
||||||
|
public bool SetupInvoked { get; private set; } = false;
|
||||||
|
|
||||||
|
public override void AdditionalSetup()
|
||||||
|
{
|
||||||
|
SetupInvoked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Logging.Testing.Tests
|
||||||
|
{
|
||||||
|
public class TestTestOutputHelper : ITestOutputHelper
|
||||||
|
{
|
||||||
|
private StringBuilder _output = new StringBuilder();
|
||||||
|
|
||||||
|
public bool Throw { get; set; }
|
||||||
|
|
||||||
|
public string Output => _output.ToString();
|
||||||
|
|
||||||
|
public void WriteLine(string message)
|
||||||
|
{
|
||||||
|
if (Throw)
|
||||||
|
{
|
||||||
|
throw new Exception("Boom!");
|
||||||
|
}
|
||||||
|
_output.AppendLine(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteLine(string format, params object[] args)
|
||||||
|
{
|
||||||
|
if (Throw)
|
||||||
|
{
|
||||||
|
throw new Exception("Boom!");
|
||||||
|
}
|
||||||
|
_output.AppendLine(string.Format(format, args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue